Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/use-dataloader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,18 @@ const failingPromise = async () => {
}

const App = () => {
useDataLoader('local-error', failingPromise, {
onError: (error) => {
useDataLoader('local-error', failingPromise, {
onError: error => {
console.log(`local onError: ${error}`)
}
},
})

useDataLoader('error', failingPromise)

return null
}

const globalOnError = (error) => {
const globalOnError = error => {
console.log(`global onError: ${error}`)
}

Expand Down Expand Up @@ -219,6 +219,7 @@ const useDataLoader = (
pollingInterval, // Relaunch the request after the last success
enabled = true, // Launch request automatically
keepPreviousData = true, // Do we need to keep the previous data after reload
maxDataLifetime, // Max time before previous success data is outdated (in millisecond)
} = {},
)
```
Expand Down
6 changes: 5 additions & 1 deletion packages/use-dataloader/src/DataLoaderProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ type UseDataLoaderInitializerArgs<T = unknown> = {
method: () => PromiseType<T>
pollingInterval?: number
keepPreviousData?: boolean
/**
* Max time before data from previous success is considered as outdated (in millisecond)
*/
maxDataLifetime?: number
}

interface Context {
Expand Down Expand Up @@ -133,7 +137,7 @@ const DataLoaderProvider = ({
[computeKey(key)]: newRequest,
}))

addReload(key, newRequest.launch)
addReload(key, () => newRequest.load(true))

return newRequest
}
Expand Down
76 changes: 64 additions & 12 deletions packages/use-dataloader/src/__tests__/dataloader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const fakeErrorPromise = () =>
})

describe('Dataloader class', () => {
test('should create instance not enabled then launch then destroy', async () => {
test('should create instance not enabled then load then destroy', async () => {
const notify = jest.fn()
const method = jest.fn(fakeSuccessPromise)
const instance = new DataLoader({
Expand All @@ -25,13 +25,13 @@ describe('Dataloader class', () => {
expect(instance.status).toBe(StatusEnum.IDLE)
expect(notify).toBeCalledTimes(0)
expect(method).toBeCalledTimes(0)
await instance.launch()
await instance.load()
expect(method).toBeCalledTimes(1)
expect(notify).toBeCalledTimes(2)
instance.destroy()
})

test('should create instance enabled then launch', async () => {
test('should create instance enabled then load', async () => {
const notify = jest.fn()
const method = jest.fn()
const instance = new DataLoader({
Expand All @@ -43,16 +43,35 @@ describe('Dataloader class', () => {
expect(instance.status).toBe(StatusEnum.LOADING)
expect(notify).toBeCalledTimes(0)
expect(method).toBeCalledTimes(0)
await instance.launch()
await instance.load()
// This does nothing because no cancel listener is set
await instance.cancel()
expect(method).toBeCalledTimes(1)
expect(notify).toBeCalledTimes(1)
})

test('should create instance with cancel listener', async () => {
test('should create instance with cancel listener and success', async () => {
const notify = jest.fn()
const method = jest.fn()
const method = jest.fn(fakeSuccessPromise)
const onCancel = jest.fn()
const instance = new DataLoader({
key: 'test',
method,
notify,
})
instance.addOnCancelListener(onCancel)
instance.addOnCancelListener(onCancel)
// eslint-disable-next-line no-void
void instance.load()
await instance.cancel()
expect(onCancel).toBeCalledTimes(1)
instance.removeOnCancelListener(onCancel)
instance.removeOnCancelListener(onCancel)
})

test('should create instance with cancel listener and error', async () => {
const notify = jest.fn()
const method = jest.fn(fakeErrorPromise)
const onCancel = jest.fn()
const instance = new DataLoader({
key: 'test',
Expand All @@ -62,7 +81,7 @@ describe('Dataloader class', () => {
instance.addOnCancelListener(onCancel)
instance.addOnCancelListener(onCancel)
// eslint-disable-next-line no-void
void instance.launch()
void instance.load()
await instance.cancel()
expect(onCancel).toBeCalledTimes(1)
instance.removeOnCancelListener(onCancel)
Expand All @@ -80,7 +99,7 @@ describe('Dataloader class', () => {
})
instance.addOnSuccessListener(onSuccess)
instance.addOnSuccessListener(onSuccess)
await instance.launch()
await instance.load()
expect(onSuccess).toBeCalledTimes(1)
instance.removeOnSuccessListener(onSuccess)
instance.removeOnSuccessListener(onSuccess)
Expand All @@ -97,7 +116,7 @@ describe('Dataloader class', () => {
})
instance.addOnErrorListener(onError)
instance.addOnErrorListener(onError)
await instance.launch()
await instance.load()
expect(onError).toBeCalledTimes(1)
expect(instance.error?.message).toBe('test')
instance.removeOnErrorListener(onError)
Expand All @@ -113,13 +132,46 @@ describe('Dataloader class', () => {
notify,
pollingInterval: PROMISE_TIMEOUT,
})
await instance.launch()
await instance.load()
expect(method).toBeCalledTimes(1)
await instance.launch()
await new Promise(resolve => setTimeout(resolve, PROMISE_TIMEOUT * 2))
expect(method).toBeCalledTimes(2)
await new Promise(resolve => setTimeout(resolve, PROMISE_TIMEOUT * 2))
expect(method).toBeCalledTimes(3)
await instance.launch()
await instance.load()
await instance.load()
expect(method).toBeCalledTimes(4)
await instance.load()
await instance.load()
await instance.load(true)
expect(method).toBeCalledTimes(6)
instance.destroy()
})

test('should update outdated data', async () => {
const notify = jest.fn()
const method = jest.fn(fakeSuccessPromise)
const onSuccess = jest.fn()
const instance = new DataLoader({
enabled: true,
key: 'test',
maxDataLifetime: PROMISE_TIMEOUT,
method,
notify,
})
instance.addOnSuccessListener(onSuccess)
expect(instance.status).toBe(StatusEnum.LOADING)
expect(method).toBeCalledTimes(0)
expect(onSuccess).toBeCalledTimes(0)
await instance.load()
expect(method).toBeCalledTimes(1)
expect(onSuccess).toBeCalledTimes(1)
await instance.load()
expect(method).toBeCalledTimes(1)
expect(onSuccess).toBeCalledTimes(1)
await new Promise(resolve => setTimeout(resolve, PROMISE_TIMEOUT * 2))
await instance.load()
expect(method).toBeCalledTimes(2)
expect(onSuccess).toBeCalledTimes(2)
})
})
53 changes: 45 additions & 8 deletions packages/use-dataloader/src/dataloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type DataLoaderConstructorArgs<T = unknown> = {
key: string
method: () => PromiseType<T>
pollingInterval?: number
maxDataLifetime?: number
keepPreviousData?: boolean
notify: (updatedRequest: DataLoader<T>) => void
}
Expand All @@ -17,12 +18,18 @@ class DataLoader<T = unknown> {

public pollingInterval?: number

public maxDataLifetime?: number

public isDataOutdated = false

private notify: (updatedRequest: DataLoader<T>) => void

public method: () => PromiseType<T>

private cancelMethod?: () => void

private canceled = false

public keepPreviousData?: boolean

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

public error?: Error

private dataOutdatedTimeout?: number

public timeout?: number

public constructor(args: DataLoaderConstructorArgs<T>) {
Expand All @@ -42,15 +51,27 @@ class DataLoader<T = unknown> {
this.pollingInterval = args?.pollingInterval
this.keepPreviousData = args?.keepPreviousData
this.notify = args.notify
this.maxDataLifetime = args.maxDataLifetime
}

public launch = async (): Promise<void> => {
try {
public load = async (force = false): Promise<void> => {
if (
force ||
this.status !== StatusEnum.SUCCESS ||
(this.status === StatusEnum.SUCCESS && this.isDataOutdated)
) {
if (this.timeout) {
// Prevent multiple call at the same time
clearTimeout(this.timeout)
}
await this.launch()
}
}

private launch = async (): Promise<void> => {
try {
if (this.status !== StatusEnum.LOADING) {
this.canceled = false
this.status = StatusEnum.LOADING
this.notify(this)
}
Expand All @@ -61,16 +82,31 @@ class DataLoader<T = unknown> {
this.status = StatusEnum.SUCCESS
this.error = undefined
this.notify(this)
await Promise.all(
this.successListeners.map(listener => listener?.(result)),
)
if (!this.canceled) {
await Promise.all(
this.successListeners.map(listener => listener?.(result)),
)

this.isDataOutdated = false
if (this.dataOutdatedTimeout) {
clearTimeout(this.dataOutdatedTimeout)
this.dataOutdatedTimeout = undefined
}
if (this.maxDataLifetime) {
this.dataOutdatedTimeout = setTimeout(() => {
this.isDataOutdated = true
}, this.maxDataLifetime) as unknown as number
}
}
} catch (err) {
this.status = StatusEnum.ERROR
this.error = err as Error
this.notify(this)
await Promise.all(
this.errorListeners.map(listener => listener?.(err as Error)),
)
if (!this.canceled) {
await Promise.all(
this.errorListeners.map(listener => listener?.(err as Error)),
)
}
}
if (this.pollingInterval) {
this.timeout = setTimeout(
Expand All @@ -82,6 +118,7 @@ class DataLoader<T = unknown> {
}

public cancel = async (): Promise<void> => {
this.canceled = true
this.cancelMethod?.()
await Promise.all(this.cancelListeners.map(listener => listener?.()))
}
Expand Down
4 changes: 4 additions & 0 deletions packages/use-dataloader/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export interface UseDataLoaderConfig<T = unknown> {
onError?: OnErrorFn
onSuccess?: OnSuccessFn
pollingInterval?: number
/**
* Max time before data from previous success is considered as outdated (in millisecond)
*/
maxDataLifetime?: number
}

/**
Expand Down
15 changes: 12 additions & 3 deletions packages/use-dataloader/src/useDataLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const useDataLoader = <T>(
onError,
onSuccess,
pollingInterval,
maxDataLifetime,
}: UseDataLoaderConfig<T> = {},
): UseDataLoaderResult<T> => {
const {
Expand All @@ -38,16 +39,24 @@ const useDataLoader = <T>(
getRequest(fetchKey) ??
addRequest(fetchKey, {
key: fetchKey,
maxDataLifetime,
method,
pollingInterval,
}),
[addRequest, fetchKey, getRequest, method, pollingInterval],
[
addRequest,
fetchKey,
getRequest,
method,
pollingInterval,
maxDataLifetime,
],
)

useEffect(() => {
if (enabled && request.status === StatusEnum.IDLE) {
// eslint-disable-next-line no-void
void request.launch()
void request.load()
}
}, [request, enabled])

Expand Down Expand Up @@ -128,7 +137,7 @@ const useDataLoader = <T>(
isPolling,
isSuccess,
previousData: previousDataRef.current,
reload: request.launch,
reload: () => request.load(true),
}
}

Expand Down