Skip to content

Commit 152642b

Browse files
feat: refresh data on max lifetime reached (#399)
1 parent f8f9002 commit 152642b

File tree

6 files changed

+135
-28
lines changed

6 files changed

+135
-28
lines changed

packages/use-dataloader/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,18 @@ const failingPromise = async () => {
7777
}
7878

7979
const App = () => {
80-
useDataLoader('local-error', failingPromise, {
81-
onError: (error) => {
80+
useDataLoader('local-error', failingPromise, {
81+
onError: error => {
8282
console.log(`local onError: ${error}`)
83-
}
83+
},
8484
})
8585

8686
useDataLoader('error', failingPromise)
8787

8888
return null
8989
}
9090

91-
const globalOnError = (error) => {
91+
const globalOnError = error => {
9292
console.log(`global onError: ${error}`)
9393
}
9494

@@ -219,6 +219,7 @@ const useDataLoader = (
219219
pollingInterval, // Relaunch the request after the last success
220220
enabled = true, // Launch request automatically
221221
keepPreviousData = true, // Do we need to keep the previous data after reload
222+
maxDataLifetime, // Max time before previous success data is outdated (in millisecond)
222223
} = {},
223224
)
224225
```

packages/use-dataloader/src/DataLoaderProvider.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ type UseDataLoaderInitializerArgs<T = unknown> = {
2424
method: () => PromiseType<T>
2525
pollingInterval?: number
2626
keepPreviousData?: boolean
27+
/**
28+
* Max time before data from previous success is considered as outdated (in millisecond)
29+
*/
30+
maxDataLifetime?: number
2731
}
2832

2933
interface Context {
@@ -133,7 +137,7 @@ const DataLoaderProvider = ({
133137
[computeKey(key)]: newRequest,
134138
}))
135139

136-
addReload(key, newRequest.launch)
140+
addReload(key, () => newRequest.load(true))
137141

138142
return newRequest
139143
}

packages/use-dataloader/src/__tests__/dataloader.test.ts

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const fakeErrorPromise = () =>
1414
})
1515

1616
describe('Dataloader class', () => {
17-
test('should create instance not enabled then launch then destroy', async () => {
17+
test('should create instance not enabled then load then destroy', async () => {
1818
const notify = jest.fn()
1919
const method = jest.fn(fakeSuccessPromise)
2020
const instance = new DataLoader({
@@ -25,13 +25,13 @@ describe('Dataloader class', () => {
2525
expect(instance.status).toBe(StatusEnum.IDLE)
2626
expect(notify).toBeCalledTimes(0)
2727
expect(method).toBeCalledTimes(0)
28-
await instance.launch()
28+
await instance.load()
2929
expect(method).toBeCalledTimes(1)
3030
expect(notify).toBeCalledTimes(2)
3131
instance.destroy()
3232
})
3333

34-
test('should create instance enabled then launch', async () => {
34+
test('should create instance enabled then load', async () => {
3535
const notify = jest.fn()
3636
const method = jest.fn()
3737
const instance = new DataLoader({
@@ -43,16 +43,35 @@ describe('Dataloader class', () => {
4343
expect(instance.status).toBe(StatusEnum.LOADING)
4444
expect(notify).toBeCalledTimes(0)
4545
expect(method).toBeCalledTimes(0)
46-
await instance.launch()
46+
await instance.load()
4747
// This does nothing because no cancel listener is set
4848
await instance.cancel()
4949
expect(method).toBeCalledTimes(1)
5050
expect(notify).toBeCalledTimes(1)
5151
})
5252

53-
test('should create instance with cancel listener', async () => {
53+
test('should create instance with cancel listener and success', async () => {
5454
const notify = jest.fn()
55-
const method = jest.fn()
55+
const method = jest.fn(fakeSuccessPromise)
56+
const onCancel = jest.fn()
57+
const instance = new DataLoader({
58+
key: 'test',
59+
method,
60+
notify,
61+
})
62+
instance.addOnCancelListener(onCancel)
63+
instance.addOnCancelListener(onCancel)
64+
// eslint-disable-next-line no-void
65+
void instance.load()
66+
await instance.cancel()
67+
expect(onCancel).toBeCalledTimes(1)
68+
instance.removeOnCancelListener(onCancel)
69+
instance.removeOnCancelListener(onCancel)
70+
})
71+
72+
test('should create instance with cancel listener and error', async () => {
73+
const notify = jest.fn()
74+
const method = jest.fn(fakeErrorPromise)
5675
const onCancel = jest.fn()
5776
const instance = new DataLoader({
5877
key: 'test',
@@ -62,7 +81,7 @@ describe('Dataloader class', () => {
6281
instance.addOnCancelListener(onCancel)
6382
instance.addOnCancelListener(onCancel)
6483
// eslint-disable-next-line no-void
65-
void instance.launch()
84+
void instance.load()
6685
await instance.cancel()
6786
expect(onCancel).toBeCalledTimes(1)
6887
instance.removeOnCancelListener(onCancel)
@@ -80,7 +99,7 @@ describe('Dataloader class', () => {
8099
})
81100
instance.addOnSuccessListener(onSuccess)
82101
instance.addOnSuccessListener(onSuccess)
83-
await instance.launch()
102+
await instance.load()
84103
expect(onSuccess).toBeCalledTimes(1)
85104
instance.removeOnSuccessListener(onSuccess)
86105
instance.removeOnSuccessListener(onSuccess)
@@ -97,7 +116,7 @@ describe('Dataloader class', () => {
97116
})
98117
instance.addOnErrorListener(onError)
99118
instance.addOnErrorListener(onError)
100-
await instance.launch()
119+
await instance.load()
101120
expect(onError).toBeCalledTimes(1)
102121
expect(instance.error?.message).toBe('test')
103122
instance.removeOnErrorListener(onError)
@@ -113,13 +132,46 @@ describe('Dataloader class', () => {
113132
notify,
114133
pollingInterval: PROMISE_TIMEOUT,
115134
})
116-
await instance.launch()
135+
await instance.load()
117136
expect(method).toBeCalledTimes(1)
118-
await instance.launch()
137+
await new Promise(resolve => setTimeout(resolve, PROMISE_TIMEOUT * 2))
119138
expect(method).toBeCalledTimes(2)
120139
await new Promise(resolve => setTimeout(resolve, PROMISE_TIMEOUT * 2))
121140
expect(method).toBeCalledTimes(3)
122-
await instance.launch()
141+
await instance.load()
142+
await instance.load()
143+
expect(method).toBeCalledTimes(4)
144+
await instance.load()
145+
await instance.load()
146+
await instance.load(true)
147+
expect(method).toBeCalledTimes(6)
123148
instance.destroy()
124149
})
150+
151+
test('should update outdated data', async () => {
152+
const notify = jest.fn()
153+
const method = jest.fn(fakeSuccessPromise)
154+
const onSuccess = jest.fn()
155+
const instance = new DataLoader({
156+
enabled: true,
157+
key: 'test',
158+
maxDataLifetime: PROMISE_TIMEOUT,
159+
method,
160+
notify,
161+
})
162+
instance.addOnSuccessListener(onSuccess)
163+
expect(instance.status).toBe(StatusEnum.LOADING)
164+
expect(method).toBeCalledTimes(0)
165+
expect(onSuccess).toBeCalledTimes(0)
166+
await instance.load()
167+
expect(method).toBeCalledTimes(1)
168+
expect(onSuccess).toBeCalledTimes(1)
169+
await instance.load()
170+
expect(method).toBeCalledTimes(1)
171+
expect(onSuccess).toBeCalledTimes(1)
172+
await new Promise(resolve => setTimeout(resolve, PROMISE_TIMEOUT * 2))
173+
await instance.load()
174+
expect(method).toBeCalledTimes(2)
175+
expect(onSuccess).toBeCalledTimes(2)
176+
})
125177
})

packages/use-dataloader/src/dataloader.ts

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type DataLoaderConstructorArgs<T = unknown> = {
66
key: string
77
method: () => PromiseType<T>
88
pollingInterval?: number
9+
maxDataLifetime?: number
910
keepPreviousData?: boolean
1011
notify: (updatedRequest: DataLoader<T>) => void
1112
}
@@ -17,12 +18,18 @@ class DataLoader<T = unknown> {
1718

1819
public pollingInterval?: number
1920

21+
public maxDataLifetime?: number
22+
23+
public isDataOutdated = false
24+
2025
private notify: (updatedRequest: DataLoader<T>) => void
2126

2227
public method: () => PromiseType<T>
2328

2429
private cancelMethod?: () => void
2530

31+
private canceled = false
32+
2633
public keepPreviousData?: boolean
2734

2835
private errorListeners: Array<OnErrorFn> = []
@@ -33,6 +40,8 @@ class DataLoader<T = unknown> {
3340

3441
public error?: Error
3542

43+
private dataOutdatedTimeout?: number
44+
3645
public timeout?: number
3746

3847
public constructor(args: DataLoaderConstructorArgs<T>) {
@@ -42,15 +51,27 @@ class DataLoader<T = unknown> {
4251
this.pollingInterval = args?.pollingInterval
4352
this.keepPreviousData = args?.keepPreviousData
4453
this.notify = args.notify
54+
this.maxDataLifetime = args.maxDataLifetime
4555
}
4656

47-
public launch = async (): Promise<void> => {
48-
try {
57+
public load = async (force = false): Promise<void> => {
58+
if (
59+
force ||
60+
this.status !== StatusEnum.SUCCESS ||
61+
(this.status === StatusEnum.SUCCESS && this.isDataOutdated)
62+
) {
4963
if (this.timeout) {
5064
// Prevent multiple call at the same time
5165
clearTimeout(this.timeout)
5266
}
67+
await this.launch()
68+
}
69+
}
70+
71+
private launch = async (): Promise<void> => {
72+
try {
5373
if (this.status !== StatusEnum.LOADING) {
74+
this.canceled = false
5475
this.status = StatusEnum.LOADING
5576
this.notify(this)
5677
}
@@ -61,16 +82,31 @@ class DataLoader<T = unknown> {
6182
this.status = StatusEnum.SUCCESS
6283
this.error = undefined
6384
this.notify(this)
64-
await Promise.all(
65-
this.successListeners.map(listener => listener?.(result)),
66-
)
85+
if (!this.canceled) {
86+
await Promise.all(
87+
this.successListeners.map(listener => listener?.(result)),
88+
)
89+
90+
this.isDataOutdated = false
91+
if (this.dataOutdatedTimeout) {
92+
clearTimeout(this.dataOutdatedTimeout)
93+
this.dataOutdatedTimeout = undefined
94+
}
95+
if (this.maxDataLifetime) {
96+
this.dataOutdatedTimeout = setTimeout(() => {
97+
this.isDataOutdated = true
98+
}, this.maxDataLifetime) as unknown as number
99+
}
100+
}
67101
} catch (err) {
68102
this.status = StatusEnum.ERROR
69103
this.error = err as Error
70104
this.notify(this)
71-
await Promise.all(
72-
this.errorListeners.map(listener => listener?.(err as Error)),
73-
)
105+
if (!this.canceled) {
106+
await Promise.all(
107+
this.errorListeners.map(listener => listener?.(err as Error)),
108+
)
109+
}
74110
}
75111
if (this.pollingInterval) {
76112
this.timeout = setTimeout(
@@ -82,6 +118,7 @@ class DataLoader<T = unknown> {
82118
}
83119

84120
public cancel = async (): Promise<void> => {
121+
this.canceled = true
85122
this.cancelMethod?.()
86123
await Promise.all(this.cancelListeners.map(listener => listener?.()))
87124
}

packages/use-dataloader/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export interface UseDataLoaderConfig<T = unknown> {
2424
onError?: OnErrorFn
2525
onSuccess?: OnSuccessFn
2626
pollingInterval?: number
27+
/**
28+
* Max time before data from previous success is considered as outdated (in millisecond)
29+
*/
30+
maxDataLifetime?: number
2731
}
2832

2933
/**

packages/use-dataloader/src/useDataLoader.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const useDataLoader = <T>(
1919
onError,
2020
onSuccess,
2121
pollingInterval,
22+
maxDataLifetime,
2223
}: UseDataLoaderConfig<T> = {},
2324
): UseDataLoaderResult<T> => {
2425
const {
@@ -38,16 +39,24 @@ const useDataLoader = <T>(
3839
getRequest(fetchKey) ??
3940
addRequest(fetchKey, {
4041
key: fetchKey,
42+
maxDataLifetime,
4143
method,
4244
pollingInterval,
4345
}),
44-
[addRequest, fetchKey, getRequest, method, pollingInterval],
46+
[
47+
addRequest,
48+
fetchKey,
49+
getRequest,
50+
method,
51+
pollingInterval,
52+
maxDataLifetime,
53+
],
4554
)
4655

4756
useEffect(() => {
4857
if (enabled && request.status === StatusEnum.IDLE) {
4958
// eslint-disable-next-line no-void
50-
void request.launch()
59+
void request.load()
5160
}
5261
}, [request, enabled])
5362

@@ -128,7 +137,7 @@ const useDataLoader = <T>(
128137
isPolling,
129138
isSuccess,
130139
previousData: previousDataRef.current,
131-
reload: request.launch,
140+
reload: () => request.load(true),
132141
}
133142
}
134143

0 commit comments

Comments
 (0)