@navios/di-react 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +14 -67
- package/lib/index.d.mts +3 -57
- package/lib/index.d.mts.map +1 -1
- package/lib/index.d.ts +8 -62
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +41 -51
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +42 -51
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/hooks/__tests__/use-container.spec.mts +3 -2
- package/src/hooks/__tests__/use-invalidate.spec.mts +10 -146
- package/src/hooks/__tests__/use-service.spec.mts +2 -2
- package/src/hooks/index.mts +1 -1
- package/src/hooks/use-container.mts +6 -3
- package/src/hooks/use-invalidate.mts +1 -82
- package/src/hooks/use-optional-service.mts +17 -25
- package/src/hooks/use-service.mts +25 -26
- package/src/hooks/use-suspense-service.mts +10 -17
- package/src/providers/__tests__/scope-provider.spec.mts +5 -2
- package/src/providers/scope-provider.mts +1 -8
|
@@ -1,154 +1,13 @@
|
|
|
1
|
-
import { Container, Injectable,
|
|
1
|
+
import { Container, Injectable, Registry } from '@navios/di'
|
|
2
2
|
|
|
3
3
|
import { act, render, screen, waitFor } from '@testing-library/react'
|
|
4
|
-
import { createElement
|
|
4
|
+
import { createElement } from 'react'
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
-
import { z } from 'zod/v4'
|
|
7
6
|
|
|
8
7
|
import { ContainerProvider } from '../../providers/container-provider.mjs'
|
|
9
|
-
import {
|
|
8
|
+
import { useInvalidateInstance } from '../use-invalidate.mjs'
|
|
10
9
|
import { useService } from '../use-service.mjs'
|
|
11
10
|
|
|
12
|
-
describe('useInvalidate', () => {
|
|
13
|
-
let container: Container
|
|
14
|
-
let registry: Registry
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
registry = new Registry()
|
|
18
|
-
container = new Container(registry)
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
afterEach(async () => {
|
|
22
|
-
await container.dispose()
|
|
23
|
-
vi.clearAllMocks()
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
const createWrapper = (children: React.ReactNode) =>
|
|
27
|
-
createElement(ContainerProvider, { container, children })
|
|
28
|
-
|
|
29
|
-
describe('with class tokens', () => {
|
|
30
|
-
it('should invalidate a service and trigger re-fetch', async () => {
|
|
31
|
-
let instanceCount = 0
|
|
32
|
-
|
|
33
|
-
@Injectable({ registry })
|
|
34
|
-
class CounterService {
|
|
35
|
-
public readonly id: number
|
|
36
|
-
|
|
37
|
-
constructor() {
|
|
38
|
-
instanceCount++
|
|
39
|
-
this.id = instanceCount
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
let invalidateFn: (() => Promise<void>) | null = null
|
|
44
|
-
|
|
45
|
-
function TestComponent() {
|
|
46
|
-
const { data, isSuccess } = useService(CounterService)
|
|
47
|
-
const invalidate = useInvalidate(CounterService)
|
|
48
|
-
invalidateFn = invalidate
|
|
49
|
-
|
|
50
|
-
if (!isSuccess) {
|
|
51
|
-
return createElement('div', { 'data-testid': 'loading' }, 'Loading...')
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return createElement('div', { 'data-testid': 'counter' }, String(data!.id))
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
render(createWrapper(createElement(TestComponent)))
|
|
58
|
-
|
|
59
|
-
// Wait for initial load
|
|
60
|
-
await waitFor(() => {
|
|
61
|
-
expect(screen.getByTestId('counter')).toBeDefined()
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
expect(screen.getByTestId('counter').textContent).toBe('1')
|
|
65
|
-
|
|
66
|
-
// Invalidate the service
|
|
67
|
-
await act(async () => {
|
|
68
|
-
await invalidateFn!()
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
// Wait for re-fetch
|
|
72
|
-
await waitFor(() => {
|
|
73
|
-
expect(screen.getByTestId('counter').textContent).toBe('2')
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
expect(instanceCount).toBe(2)
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
describe('with injection tokens and args', () => {
|
|
81
|
-
it('should invalidate a service with specific args', async () => {
|
|
82
|
-
let instanceCount = 0
|
|
83
|
-
const instances = new Map<string, number>()
|
|
84
|
-
|
|
85
|
-
const UserSchema = z.object({ userId: z.string() })
|
|
86
|
-
const UserToken = InjectionToken.create<
|
|
87
|
-
{ userId: string; instanceId: number },
|
|
88
|
-
typeof UserSchema
|
|
89
|
-
>('User', UserSchema)
|
|
90
|
-
|
|
91
|
-
@Injectable({ registry, token: UserToken })
|
|
92
|
-
class _UserService {
|
|
93
|
-
public userId: string
|
|
94
|
-
public instanceId: number
|
|
95
|
-
|
|
96
|
-
constructor(args: z.infer<typeof UserSchema>) {
|
|
97
|
-
this.userId = args.userId
|
|
98
|
-
const currentCount = instances.get(args.userId) ?? 0
|
|
99
|
-
instanceCount++
|
|
100
|
-
this.instanceId = instanceCount
|
|
101
|
-
instances.set(args.userId, this.instanceId)
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let invalidateUser1: (() => Promise<void>) | null = null
|
|
106
|
-
|
|
107
|
-
function TestComponent() {
|
|
108
|
-
const args1 = useMemo(() => ({ userId: 'user1' }), [])
|
|
109
|
-
const args2 = useMemo(() => ({ userId: 'user2' }), [])
|
|
110
|
-
|
|
111
|
-
const { data: user1, isSuccess: isSuccess1 } = useService(UserToken, args1)
|
|
112
|
-
const { data: user2, isSuccess: isSuccess2 } = useService(UserToken, args2)
|
|
113
|
-
const invalidate1 = useInvalidate(UserToken, args1)
|
|
114
|
-
invalidateUser1 = invalidate1
|
|
115
|
-
|
|
116
|
-
if (!isSuccess1 || !isSuccess2) {
|
|
117
|
-
return createElement('div', { 'data-testid': 'loading' }, 'Loading...')
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return createElement('div', null, [
|
|
121
|
-
createElement('span', { key: '1', 'data-testid': 'user1' }, String(user1!.instanceId)),
|
|
122
|
-
createElement('span', { key: '2', 'data-testid': 'user2' }, String(user2!.instanceId)),
|
|
123
|
-
])
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
render(createWrapper(createElement(TestComponent)))
|
|
127
|
-
|
|
128
|
-
await waitFor(() => {
|
|
129
|
-
expect(screen.getByTestId('user1')).toBeDefined()
|
|
130
|
-
expect(screen.getByTestId('user2')).toBeDefined()
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
expect(screen.getByTestId('user1').textContent).toBe('1')
|
|
134
|
-
expect(screen.getByTestId('user2').textContent).toBe('2')
|
|
135
|
-
|
|
136
|
-
// Invalidate only user1
|
|
137
|
-
await act(async () => {
|
|
138
|
-
await invalidateUser1!()
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
// Wait for user1 to be re-fetched
|
|
142
|
-
await waitFor(() => {
|
|
143
|
-
expect(screen.getByTestId('user1').textContent).toBe('3')
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
// user2 should still be the same
|
|
147
|
-
expect(screen.getByTestId('user2').textContent).toBe('2')
|
|
148
|
-
})
|
|
149
|
-
})
|
|
150
|
-
})
|
|
151
|
-
|
|
152
11
|
describe('useInvalidateInstance', () => {
|
|
153
12
|
let container: Container
|
|
154
13
|
let registry: Registry
|
|
@@ -179,7 +38,8 @@ describe('useInvalidateInstance', () => {
|
|
|
179
38
|
}
|
|
180
39
|
}
|
|
181
40
|
|
|
182
|
-
let invalidateInstanceFn: ((instance: unknown) => Promise<void>) | null =
|
|
41
|
+
let invalidateInstanceFn: ((instance: unknown) => Promise<void>) | null =
|
|
42
|
+
null
|
|
183
43
|
let currentInstance: CounterService | undefined
|
|
184
44
|
|
|
185
45
|
function TestComponent() {
|
|
@@ -192,7 +52,11 @@ describe('useInvalidateInstance', () => {
|
|
|
192
52
|
return createElement('div', { 'data-testid': 'loading' }, 'Loading...')
|
|
193
53
|
}
|
|
194
54
|
|
|
195
|
-
return createElement(
|
|
55
|
+
return createElement(
|
|
56
|
+
'div',
|
|
57
|
+
{ 'data-testid': 'counter' },
|
|
58
|
+
String(data!.id),
|
|
59
|
+
)
|
|
196
60
|
}
|
|
197
61
|
|
|
198
62
|
render(createWrapper(createElement(TestComponent)))
|
|
@@ -18,7 +18,7 @@ describe('useService', () => {
|
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
afterEach(async () => {
|
|
21
|
-
await container.
|
|
21
|
+
await container.dispose()
|
|
22
22
|
})
|
|
23
23
|
|
|
24
24
|
const createWrapper = () => {
|
|
@@ -79,7 +79,7 @@ describe('useService', () => {
|
|
|
79
79
|
expect(result.current.isSuccess).toBe(false)
|
|
80
80
|
})
|
|
81
81
|
|
|
82
|
-
it
|
|
82
|
+
it('should refetch service when refetch is called', async () => {
|
|
83
83
|
let callCount = 0
|
|
84
84
|
|
|
85
85
|
@Injectable({ registry })
|
package/src/hooks/index.mts
CHANGED
|
@@ -4,7 +4,7 @@ export type { UseServiceResult } from './use-service.mjs'
|
|
|
4
4
|
export { useSuspenseService } from './use-suspense-service.mjs'
|
|
5
5
|
export { useOptionalService } from './use-optional-service.mjs'
|
|
6
6
|
export type { UseOptionalServiceResult } from './use-optional-service.mjs'
|
|
7
|
-
export {
|
|
7
|
+
export { useInvalidateInstance } from './use-invalidate.mjs'
|
|
8
8
|
export {
|
|
9
9
|
useScope,
|
|
10
10
|
useScopeOrThrow,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type { Container,
|
|
1
|
+
import type { Container, ScopedContainer } from '@navios/di'
|
|
2
2
|
|
|
3
3
|
import { useContext } from 'react'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ContainerContext,
|
|
7
|
+
ScopedContainerContext,
|
|
8
|
+
} from '../providers/context.mjs'
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Hook to get the current container (ScopedContainer if inside ScopeProvider, otherwise Container).
|
|
@@ -14,7 +17,7 @@ import { ContainerContext, ScopedContainerContext } from '../providers/context.m
|
|
|
14
17
|
*
|
|
15
18
|
* @returns The current container (ScopedContainer or Container)
|
|
16
19
|
*/
|
|
17
|
-
export function useContainer():
|
|
20
|
+
export function useContainer(): Container | ScopedContainer {
|
|
18
21
|
const scopedContainer = useContext(ScopedContainerContext)
|
|
19
22
|
const container = useContext(ContainerContext)
|
|
20
23
|
|
|
@@ -1,87 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BoundInjectionToken,
|
|
3
|
-
ClassType,
|
|
4
|
-
FactoryInjectionToken,
|
|
5
|
-
InjectionToken,
|
|
6
|
-
InjectionTokenSchemaType,
|
|
7
|
-
} from '@navios/di'
|
|
8
|
-
|
|
9
1
|
import { useCallback } from 'react'
|
|
10
2
|
|
|
11
|
-
import { useContainer
|
|
12
|
-
|
|
13
|
-
type InvalidatableToken =
|
|
14
|
-
| ClassType
|
|
15
|
-
| InjectionToken<any, any>
|
|
16
|
-
| BoundInjectionToken<any, any>
|
|
17
|
-
| FactoryInjectionToken<any, any>
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Hook that returns a function to invalidate a service by its token.
|
|
21
|
-
*
|
|
22
|
-
* When called, this will:
|
|
23
|
-
* 1. Destroy the current service instance
|
|
24
|
-
* 2. Trigger re-fetch in all components using useService/useSuspenseService for that token
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```tsx
|
|
28
|
-
* function UserProfile() {
|
|
29
|
-
* const { data: user } = useService(UserService)
|
|
30
|
-
* const invalidateUser = useInvalidate(UserService)
|
|
31
|
-
*
|
|
32
|
-
* const handleRefresh = () => {
|
|
33
|
-
* invalidateUser() // All components using UserService will re-fetch
|
|
34
|
-
* }
|
|
35
|
-
*
|
|
36
|
-
* return (
|
|
37
|
-
* <div>
|
|
38
|
-
* <span>{user?.name}</span>
|
|
39
|
-
* <button onClick={handleRefresh}>Refresh</button>
|
|
40
|
-
* </div>
|
|
41
|
-
* )
|
|
42
|
-
* }
|
|
43
|
-
* ```
|
|
44
|
-
*/
|
|
45
|
-
export function useInvalidate<T extends InvalidatableToken>(
|
|
46
|
-
token: T,
|
|
47
|
-
): () => Promise<void>
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Hook that returns a function to invalidate a service by its token with args.
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* ```tsx
|
|
54
|
-
* function UserProfile({ userId }: { userId: string }) {
|
|
55
|
-
* const args = useMemo(() => ({ userId }), [userId])
|
|
56
|
-
* const { data: user } = useService(UserToken, args)
|
|
57
|
-
* const invalidateUser = useInvalidate(UserToken, args)
|
|
58
|
-
*
|
|
59
|
-
* return (
|
|
60
|
-
* <div>
|
|
61
|
-
* <span>{user?.name}</span>
|
|
62
|
-
* <button onClick={() => invalidateUser()}>Refresh</button>
|
|
63
|
-
* </div>
|
|
64
|
-
* )
|
|
65
|
-
* }
|
|
66
|
-
* ```
|
|
67
|
-
*/
|
|
68
|
-
export function useInvalidate<T, S extends InjectionTokenSchemaType>(
|
|
69
|
-
token: InjectionToken<T, S>,
|
|
70
|
-
args: S extends undefined ? never : unknown,
|
|
71
|
-
): () => Promise<void>
|
|
72
|
-
|
|
73
|
-
export function useInvalidate(
|
|
74
|
-
token: InvalidatableToken,
|
|
75
|
-
args?: unknown,
|
|
76
|
-
): () => Promise<void> {
|
|
77
|
-
const rootContainer = useRootContainer()
|
|
78
|
-
const serviceLocator = rootContainer.getServiceLocator()
|
|
79
|
-
|
|
80
|
-
return useCallback(async () => {
|
|
81
|
-
const instanceName = serviceLocator.getInstanceIdentifier(token, args)
|
|
82
|
-
await serviceLocator.invalidate(instanceName)
|
|
83
|
-
}, [serviceLocator, token, args])
|
|
84
|
-
}
|
|
3
|
+
import { useContainer } from './use-container.mjs'
|
|
85
4
|
|
|
86
5
|
/**
|
|
87
6
|
* Hook that returns a function to invalidate a service instance directly.
|
|
@@ -155,7 +155,6 @@ export function useOptionalService(
|
|
|
155
155
|
// useContainer returns ScopedContainer if inside ScopeProvider, otherwise Container
|
|
156
156
|
const container = useContainer()
|
|
157
157
|
const rootContainer = useRootContainer()
|
|
158
|
-
const serviceLocator = rootContainer.getServiceLocator()
|
|
159
158
|
|
|
160
159
|
// Try to get the instance synchronously first for better performance
|
|
161
160
|
// This avoids the async loading state when the instance is already cached
|
|
@@ -206,12 +205,11 @@ export function useOptionalService(
|
|
|
206
205
|
token as AnyInjectableType,
|
|
207
206
|
args as any,
|
|
208
207
|
)
|
|
209
|
-
|
|
210
208
|
// Get instance name for event subscription
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
209
|
+
const instanceName = container.calculateInstanceName(token, args)
|
|
210
|
+
if (instanceName) {
|
|
211
|
+
instanceNameRef.current = instanceName
|
|
212
|
+
}
|
|
215
213
|
dispatch({ type: 'success', data: instance })
|
|
216
214
|
} catch (error) {
|
|
217
215
|
// Caught exceptions are treated as errors
|
|
@@ -227,42 +225,36 @@ export function useOptionalService(
|
|
|
227
225
|
dispatch({ type: 'error', error: err })
|
|
228
226
|
}
|
|
229
227
|
}
|
|
230
|
-
}, [container,
|
|
228
|
+
}, [container, token, args])
|
|
231
229
|
|
|
232
230
|
// Subscribe to invalidation events
|
|
233
231
|
useEffect(() => {
|
|
234
|
-
const eventBus =
|
|
232
|
+
const eventBus = rootContainer.getEventBus()
|
|
235
233
|
let unsubscribe: (() => void) | undefined
|
|
236
234
|
|
|
237
235
|
// If we already have a sync instance from initial render, just set up subscription
|
|
238
236
|
// Otherwise, fetch async
|
|
239
237
|
const syncInstance = initialSyncInstanceRef.current
|
|
240
238
|
if (syncInstance) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
239
|
+
const instanceName = container.calculateInstanceName(token, args)
|
|
240
|
+
if (instanceName) {
|
|
241
|
+
instanceNameRef.current = instanceName
|
|
242
|
+
unsubscribe = eventBus.on(instanceName, 'destroy', () => {
|
|
243
|
+
// Re-fetch when the service is invalidated
|
|
244
|
+
void fetchService()
|
|
245
|
+
})
|
|
246
|
+
}
|
|
248
247
|
} else {
|
|
249
|
-
void fetchService()
|
|
250
|
-
|
|
251
|
-
// Set up subscription after we have the instance name
|
|
252
|
-
const setupSubscription = () => {
|
|
248
|
+
void fetchService().then(() => {
|
|
253
249
|
if (instanceNameRef.current) {
|
|
254
250
|
unsubscribe = eventBus.on(instanceNameRef.current, 'destroy', () => {
|
|
255
251
|
// Re-fetch when the service is invalidated
|
|
256
252
|
void fetchService()
|
|
257
253
|
})
|
|
258
254
|
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Wait a tick for the instance name to be set
|
|
262
|
-
const timeoutId = setTimeout(setupSubscription, 10)
|
|
255
|
+
})
|
|
263
256
|
|
|
264
257
|
return () => {
|
|
265
|
-
clearTimeout(timeoutId)
|
|
266
258
|
unsubscribe?.()
|
|
267
259
|
}
|
|
268
260
|
}
|
|
@@ -270,7 +262,7 @@ export function useOptionalService(
|
|
|
270
262
|
return () => {
|
|
271
263
|
unsubscribe?.()
|
|
272
264
|
}
|
|
273
|
-
}, [fetchService,
|
|
265
|
+
}, [fetchService, rootContainer, token, args])
|
|
274
266
|
|
|
275
267
|
return {
|
|
276
268
|
data: state.status === 'success' ? state.data : undefined,
|
|
@@ -105,7 +105,6 @@ export function useService(
|
|
|
105
105
|
// This automatically handles request-scoped services correctly
|
|
106
106
|
const container = useContainer()
|
|
107
107
|
const rootContainer = useRootContainer()
|
|
108
|
-
const serviceLocator = rootContainer.getServiceLocator()
|
|
109
108
|
|
|
110
109
|
// Try to get the instance synchronously first for better performance
|
|
111
110
|
// This avoids the async loading state when the instance is already cached
|
|
@@ -146,7 +145,7 @@ export function useService(
|
|
|
146
145
|
|
|
147
146
|
// Subscribe to invalidation events
|
|
148
147
|
useEffect(() => {
|
|
149
|
-
const eventBus =
|
|
148
|
+
const eventBus = rootContainer.getEventBus()
|
|
150
149
|
let unsubscribe: (() => void) | undefined
|
|
151
150
|
let isMounted = true
|
|
152
151
|
|
|
@@ -163,22 +162,23 @@ export function useService(
|
|
|
163
162
|
if (!isMounted) return
|
|
164
163
|
|
|
165
164
|
// Get instance name for event subscription
|
|
166
|
-
const instanceName =
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
instanceNameRef.current = instanceName
|
|
165
|
+
const instanceName = container.calculateInstanceName(token, args)
|
|
166
|
+
if (instanceName) {
|
|
167
|
+
instanceNameRef.current = instanceName
|
|
168
|
+
}
|
|
171
169
|
|
|
172
170
|
dispatch({ type: 'success', data: instance })
|
|
173
171
|
|
|
174
172
|
// Set up subscription after we have the instance
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
173
|
+
if (instanceName) {
|
|
174
|
+
unsubscribe = eventBus.on(instanceName, 'destroy', () => {
|
|
175
|
+
// Re-fetch when the service is invalidated
|
|
176
|
+
if (isMounted) {
|
|
177
|
+
dispatch({ type: 'loading' })
|
|
178
|
+
void fetchAndSubscribe()
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
182
|
} catch (error) {
|
|
183
183
|
if (isMounted) {
|
|
184
184
|
dispatch({ type: 'error', error: error as Error })
|
|
@@ -190,17 +190,16 @@ export function useService(
|
|
|
190
190
|
// Otherwise, fetch async
|
|
191
191
|
const syncInstance = initialSyncInstanceRef.current
|
|
192
192
|
if (syncInstance && refetchCounter === 0) {
|
|
193
|
-
const instanceName =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
})
|
|
193
|
+
const instanceName = container.calculateInstanceName(token, args)
|
|
194
|
+
if (instanceName) {
|
|
195
|
+
instanceNameRef.current = instanceName
|
|
196
|
+
unsubscribe = eventBus.on(instanceName, 'destroy', () => {
|
|
197
|
+
if (isMounted) {
|
|
198
|
+
dispatch({ type: 'loading' })
|
|
199
|
+
void fetchAndSubscribe()
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
}
|
|
204
203
|
} else {
|
|
205
204
|
dispatch({ type: 'loading' })
|
|
206
205
|
void fetchAndSubscribe()
|
|
@@ -210,7 +209,7 @@ export function useService(
|
|
|
210
209
|
isMounted = false
|
|
211
210
|
unsubscribe?.()
|
|
212
211
|
}
|
|
213
|
-
}, [container,
|
|
212
|
+
}, [container, rootContainer, token, args, refetchCounter])
|
|
214
213
|
|
|
215
214
|
const refetch = useCallback(() => {
|
|
216
215
|
setRefetchCounter((c) => c + 1)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
AnyInjectableType,
|
|
3
2
|
BoundInjectionToken,
|
|
4
3
|
ClassType,
|
|
4
|
+
Container,
|
|
5
5
|
Factorable,
|
|
6
6
|
FactoryInjectionToken,
|
|
7
7
|
InjectionToken,
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
} from '@navios/di'
|
|
10
10
|
import type { z, ZodType } from 'zod/v4'
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react'
|
|
13
14
|
|
|
14
15
|
import type { Join, UnionToArray } from '../types.mjs'
|
|
@@ -53,13 +54,11 @@ function getCache(container: object): Map<string, CacheEntry<any>> {
|
|
|
53
54
|
*/
|
|
54
55
|
function setupInvalidationSubscription(
|
|
55
56
|
entry: CacheEntry<any>,
|
|
56
|
-
|
|
57
|
-
import('@navios/di').Container['getServiceLocator']
|
|
58
|
-
>,
|
|
57
|
+
rootContainer: Container,
|
|
59
58
|
): void {
|
|
60
59
|
if (entry.unsubscribe || !entry.instanceName) return
|
|
61
60
|
|
|
62
|
-
const eventBus =
|
|
61
|
+
const eventBus = rootContainer.getEventBus()
|
|
63
62
|
entry.unsubscribe = eventBus.on(entry.instanceName, 'destroy', () => {
|
|
64
63
|
// Clear cache and notify subscribers to re-fetch
|
|
65
64
|
entry.result = undefined
|
|
@@ -113,7 +112,6 @@ export function useSuspenseService(
|
|
|
113
112
|
// useContainer returns ScopedContainer if inside ScopeProvider, otherwise Container
|
|
114
113
|
const container = useContainer()
|
|
115
114
|
const rootContainer = useRootContainer()
|
|
116
|
-
const serviceLocator = rootContainer.getServiceLocator()
|
|
117
115
|
const cache = getCache(container)
|
|
118
116
|
const cacheKey = getCacheKey(token, args)
|
|
119
117
|
const entryRef = useRef<CacheEntry<any> | null>(null)
|
|
@@ -141,6 +139,7 @@ export function useSuspenseService(
|
|
|
141
139
|
// This avoids suspense when the instance is already cached
|
|
142
140
|
const syncInstance = container.tryGetSync(token, args)
|
|
143
141
|
|
|
142
|
+
const instanceName = container.calculateInstanceName(token, args)
|
|
144
143
|
const entry: CacheEntry<any> = {
|
|
145
144
|
promise: null,
|
|
146
145
|
result: syncInstance ?? undefined,
|
|
@@ -148,9 +147,7 @@ export function useSuspenseService(
|
|
|
148
147
|
status: syncInstance ? 'resolved' : 'pending',
|
|
149
148
|
version: 0,
|
|
150
149
|
subscribers: new Set(),
|
|
151
|
-
instanceName
|
|
152
|
-
? serviceLocator.getInstanceIdentifier(token as AnyInjectableType, args)
|
|
153
|
-
: null,
|
|
150
|
+
instanceName,
|
|
154
151
|
unsubscribe: undefined,
|
|
155
152
|
}
|
|
156
153
|
cache.set(cacheKey, entry)
|
|
@@ -170,13 +167,9 @@ export function useSuspenseService(
|
|
|
170
167
|
.then((instance: any) => {
|
|
171
168
|
currentEntry.result = instance
|
|
172
169
|
currentEntry.status = 'resolved'
|
|
173
|
-
currentEntry.instanceName = serviceLocator.getInstanceIdentifier(
|
|
174
|
-
token as AnyInjectableType,
|
|
175
|
-
args,
|
|
176
|
-
)
|
|
177
170
|
|
|
178
171
|
// Subscribe to invalidation events if not already subscribed
|
|
179
|
-
setupInvalidationSubscription(currentEntry,
|
|
172
|
+
setupInvalidationSubscription(currentEntry, rootContainer)
|
|
180
173
|
|
|
181
174
|
// Notify subscribers
|
|
182
175
|
currentEntry.subscribers.forEach((callback) => callback())
|
|
@@ -189,7 +182,7 @@ export function useSuspenseService(
|
|
|
189
182
|
})
|
|
190
183
|
|
|
191
184
|
return currentEntry.promise
|
|
192
|
-
}, [container,
|
|
185
|
+
}, [container, rootContainer, token, args])
|
|
193
186
|
|
|
194
187
|
// Subscribe to cache changes
|
|
195
188
|
const subscribe = useCallback(
|
|
@@ -219,9 +212,9 @@ export function useSuspenseService(
|
|
|
219
212
|
currentEntry.instanceName &&
|
|
220
213
|
!currentEntry.unsubscribe
|
|
221
214
|
) {
|
|
222
|
-
setupInvalidationSubscription(currentEntry,
|
|
215
|
+
setupInvalidationSubscription(currentEntry, rootContainer)
|
|
223
216
|
}
|
|
224
|
-
}, [
|
|
217
|
+
}, [rootContainer, entry])
|
|
225
218
|
|
|
226
219
|
// Start fetching if not already
|
|
227
220
|
if (entry.status === 'pending' && !entry.promise) {
|
|
@@ -4,8 +4,12 @@ import { render, screen, waitFor } from '@testing-library/react'
|
|
|
4
4
|
import { createElement } from 'react'
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
useScope,
|
|
9
|
+
useScopedContainer,
|
|
10
|
+
useScopeMetadata,
|
|
11
|
+
} from '../../hooks/use-scope.mjs'
|
|
7
12
|
import { useService } from '../../hooks/use-service.mjs'
|
|
8
|
-
import { useScope, useScopeMetadata, useScopedContainer } from '../../hooks/use-scope.mjs'
|
|
9
13
|
import { ContainerProvider } from '../container-provider.mjs'
|
|
10
14
|
import { ScopeProvider } from '../scope-provider.mjs'
|
|
11
15
|
|
|
@@ -267,7 +271,6 @@ describe('ScopeProvider', () => {
|
|
|
267
271
|
{
|
|
268
272
|
scopeId: 'test-scope',
|
|
269
273
|
metadata: { userId: '123', role: 'admin' },
|
|
270
|
-
priority: 200,
|
|
271
274
|
},
|
|
272
275
|
createElement(TestComponent),
|
|
273
276
|
),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ReactNode } from 'react'
|
|
2
1
|
import type { ScopedContainer } from '@navios/di'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
3
|
|
|
4
4
|
import { useContext, useEffect, useId, useRef } from 'react'
|
|
5
5
|
import { jsx } from 'react/jsx-runtime'
|
|
@@ -17,11 +17,6 @@ export interface ScopeProviderProps {
|
|
|
17
17
|
* Can be used to pass data like user info, request headers, etc.
|
|
18
18
|
*/
|
|
19
19
|
metadata?: Record<string, unknown>
|
|
20
|
-
/**
|
|
21
|
-
* Priority for service resolution. Higher priority scopes take precedence.
|
|
22
|
-
* @default 100
|
|
23
|
-
*/
|
|
24
|
-
priority?: number
|
|
25
20
|
children: ReactNode
|
|
26
21
|
}
|
|
27
22
|
|
|
@@ -50,7 +45,6 @@ export interface ScopeProviderProps {
|
|
|
50
45
|
export function ScopeProvider({
|
|
51
46
|
scopeId,
|
|
52
47
|
metadata,
|
|
53
|
-
priority = 100,
|
|
54
48
|
children,
|
|
55
49
|
}: ScopeProviderProps) {
|
|
56
50
|
const container = useContext(ContainerContext)
|
|
@@ -70,7 +64,6 @@ export function ScopeProvider({
|
|
|
70
64
|
scopedContainerRef.current = container.beginRequest(
|
|
71
65
|
effectiveScopeId,
|
|
72
66
|
metadata,
|
|
73
|
-
priority,
|
|
74
67
|
)
|
|
75
68
|
}
|
|
76
69
|
}
|