@croct/plug-react 0.10.0 → 0.11.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/CroctProvider.cjs +81 -0
- package/CroctProvider.d.cts +11 -0
- package/CroctProvider.d.ts +8 -4
- package/CroctProvider.js +57 -46
- package/api.cjs +22 -0
- package/{src/react-app-env.d.ts → api.d.cts} +1 -0
- package/api.d.ts +1 -0
- package/api.js +1 -18
- package/components/Personalization/index.cjs +36 -0
- package/components/Personalization/index.d.cts +13 -0
- package/components/Personalization/index.d.ts +7 -4
- package/components/Personalization/index.js +10 -11
- package/components/Slot/index.cjs +36 -0
- package/components/Slot/index.d.cts +22 -0
- package/components/Slot/index.d.ts +9 -6
- package/components/Slot/index.js +10 -12
- package/components/index.cjs +24 -0
- package/components/index.d.cts +9 -0
- package/components/index.d.ts +9 -2
- package/components/index.js +2 -19
- package/global.d.cjs +1 -0
- package/{src/global.d.ts → global.d.d.cts} +1 -1
- package/global.d.d.ts +7 -0
- package/global.d.js +0 -0
- package/hash.cjs +36 -0
- package/hash.d.cts +2 -0
- package/hash.d.ts +2 -1
- package/hash.js +10 -11
- package/hooks/Cache.cjs +88 -0
- package/hooks/Cache.d.cts +9 -0
- package/hooks/Cache.d.ts +4 -17
- package/hooks/Cache.js +58 -56
- package/hooks/index.cjs +26 -0
- package/hooks/index.d.cts +8 -0
- package/hooks/index.d.ts +8 -3
- package/hooks/index.js +3 -20
- package/hooks/useContent.cjs +92 -0
- package/hooks/useContent.d.cts +20 -0
- package/hooks/useContent.d.ts +6 -5
- package/hooks/useContent.js +65 -42
- package/hooks/useCroct.cjs +38 -0
- package/hooks/useCroct.d.cts +5 -0
- package/hooks/useCroct.d.ts +4 -1
- package/hooks/useCroct.js +12 -12
- package/hooks/useEvaluation.cjs +85 -0
- package/hooks/useEvaluation.d.cts +14 -0
- package/hooks/useEvaluation.d.ts +5 -3
- package/hooks/useEvaluation.js +54 -45
- package/hooks/useLoader.cjs +82 -0
- package/hooks/useLoader.d.cts +7 -0
- package/hooks/useLoader.d.ts +5 -3
- package/hooks/useLoader.js +54 -59
- package/index.cjs +32 -0
- package/index.d.cts +13 -0
- package/index.d.ts +10 -3
- package/index.js +6 -23
- package/package.json +42 -11
- package/react-app-env.d.cjs +1 -0
- package/react-app-env.d.d.cts +2 -0
- package/react-app-env.d.d.ts +2 -0
- package/react-app-env.d.js +0 -0
- package/ssr-polyfills.cjs +86 -0
- package/ssr-polyfills.d.cts +2 -0
- package/ssr-polyfills.d.ts +2 -3
- package/ssr-polyfills.js +49 -64
- package/CroctProvider.js.map +0 -1
- package/api.js.map +0 -1
- package/components/Personalization/index.js.map +0 -1
- package/components/Slot/index.js.map +0 -1
- package/components/index.js.map +0 -1
- package/hash.js.map +0 -1
- package/hooks/Cache.js.map +0 -1
- package/hooks/index.js.map +0 -1
- package/hooks/useContent.js.map +0 -1
- package/hooks/useCroct.js.map +0 -1
- package/hooks/useEvaluation.js.map +0 -1
- package/hooks/useLoader.js.map +0 -1
- package/index.js.map +0 -1
- package/src/api.ts +0 -1
- package/src/components/index.ts +0 -2
- package/src/hash.test.ts +0 -22
- package/src/hash.ts +0 -12
- package/src/hooks/Cache.test.ts +0 -280
- package/src/hooks/Cache.ts +0 -97
- package/src/hooks/index.ts +0 -3
- package/src/hooks/useContent.ssr.test.ts +0 -23
- package/src/hooks/useContent.test.ts +0 -183
- package/src/hooks/useContent.ts +0 -107
- package/src/hooks/useCroct.ts +0 -16
- package/src/hooks/useEvaluation.ssr.test.ts +0 -23
- package/src/hooks/useEvaluation.test.ts +0 -180
- package/src/hooks/useEvaluation.ts +0 -94
- package/src/hooks/useLoader.test.ts +0 -407
- package/src/hooks/useLoader.ts +0 -84
- package/src/index.ts +0 -6
- package/src/ssr-polyfills.ssr.test.ts +0 -46
- package/src/ssr-polyfills.test.ts +0 -65
- package/src/ssr-polyfills.ts +0 -70
- package/ssr-polyfills.js.map +0 -1
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import {JsonValue} from '@croct/plug/sdk/json';
|
|
2
|
-
import {EvaluationOptions} from '@croct/sdk/facade/evaluatorFacade';
|
|
3
|
-
import {useEffect, useState} from 'react';
|
|
4
|
-
import {useLoader} from './useLoader';
|
|
5
|
-
import {useCroct} from './useCroct';
|
|
6
|
-
import {isSsr} from '../ssr-polyfills';
|
|
7
|
-
import {hash} from '../hash';
|
|
8
|
-
|
|
9
|
-
export type UseEvaluationOptions<I, F> = EvaluationOptions & {
|
|
10
|
-
initial?: I,
|
|
11
|
-
fallback?: F,
|
|
12
|
-
cacheKey?: string,
|
|
13
|
-
expiration?: number,
|
|
14
|
-
staleWhileLoading?: boolean,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
type UseEvaluationHook = <T extends JsonValue, I = T, F = T>(
|
|
18
|
-
query: string,
|
|
19
|
-
options?: UseEvaluationOptions<I, F>,
|
|
20
|
-
) => T | I | F;
|
|
21
|
-
|
|
22
|
-
function useCsrEvaluation<T = JsonValue, I = T, F = T>(
|
|
23
|
-
query: string,
|
|
24
|
-
options: UseEvaluationOptions<I, F> = {},
|
|
25
|
-
): T | I | F {
|
|
26
|
-
const {
|
|
27
|
-
cacheKey,
|
|
28
|
-
fallback,
|
|
29
|
-
expiration,
|
|
30
|
-
staleWhileLoading = false,
|
|
31
|
-
initial: initialValue,
|
|
32
|
-
...evaluationOptions
|
|
33
|
-
} = options;
|
|
34
|
-
|
|
35
|
-
const [initial, setInitial] = useState<T | I | F | undefined>(initialValue);
|
|
36
|
-
const croct = useCroct();
|
|
37
|
-
|
|
38
|
-
const result = useLoader<T | I | F>({
|
|
39
|
-
cacheKey: hash(
|
|
40
|
-
`useEvaluation:${cacheKey ?? ''}`
|
|
41
|
-
+ `:${query}`
|
|
42
|
-
+ `:${JSON.stringify(options.attributes ?? {})}`,
|
|
43
|
-
),
|
|
44
|
-
loader: () => croct.evaluate<T & JsonValue>(query, cleanEvaluationOptions(evaluationOptions)),
|
|
45
|
-
initial: initial,
|
|
46
|
-
fallback: fallback,
|
|
47
|
-
expiration: expiration,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
useEffect(
|
|
51
|
-
() => {
|
|
52
|
-
if (staleWhileLoading) {
|
|
53
|
-
setInitial(current => {
|
|
54
|
-
if (current !== result) {
|
|
55
|
-
return result;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return current;
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
[result, staleWhileLoading],
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
return result;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function cleanEvaluationOptions(options: EvaluationOptions): EvaluationOptions {
|
|
69
|
-
const result: EvaluationOptions = {};
|
|
70
|
-
|
|
71
|
-
for (const [key, value] of Object.entries(options) as Array<[keyof EvaluationOptions, any]>) {
|
|
72
|
-
if (value !== undefined) {
|
|
73
|
-
result[key] = value;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return result;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function useSsrEvaluation<T = JsonValue, I = T, F = T>(
|
|
81
|
-
_: string,
|
|
82
|
-
{initial}: UseEvaluationOptions<I, F> = {},
|
|
83
|
-
): T | I | F {
|
|
84
|
-
if (initial === undefined) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
'The initial value is required for server-side rendering (SSR). '
|
|
87
|
-
+ 'For help, see https://croct.help/sdk/react/missing-evaluation-result',
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return initial;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export const useEvaluation: UseEvaluationHook = isSsr() ? useSsrEvaluation : useCsrEvaluation;
|
|
@@ -1,407 +0,0 @@
|
|
|
1
|
-
import {act, renderHook, waitFor} from '@testing-library/react';
|
|
2
|
-
import {useLoader} from './useLoader';
|
|
3
|
-
|
|
4
|
-
describe('useLoader', () => {
|
|
5
|
-
const cacheKey = {
|
|
6
|
-
index: 0,
|
|
7
|
-
next: function next(): string {
|
|
8
|
-
this.index++;
|
|
9
|
-
|
|
10
|
-
return this.current();
|
|
11
|
-
},
|
|
12
|
-
current: function current(): string {
|
|
13
|
-
return `key-${this.index}`;
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
cacheKey.next();
|
|
19
|
-
jest.resetAllMocks();
|
|
20
|
-
jest.clearAllTimers();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// Needed to use fake timers and promises:
|
|
24
|
-
// https://github.com/testing-library/react-testing-library/issues/244#issuecomment-449461804
|
|
25
|
-
function flushPromises(): Promise<void> {
|
|
26
|
-
return Promise.resolve();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
it('should return the load the value and cache on success', async () => {
|
|
30
|
-
const loader = jest.fn().mockResolvedValue('foo');
|
|
31
|
-
|
|
32
|
-
const {result, rerender} = renderHook(
|
|
33
|
-
() => useLoader({
|
|
34
|
-
cacheKey: cacheKey.current(),
|
|
35
|
-
loader: loader,
|
|
36
|
-
}),
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
rerender();
|
|
40
|
-
|
|
41
|
-
await waitFor(() => expect(result.current).toBe('foo'));
|
|
42
|
-
|
|
43
|
-
expect(loader).toHaveBeenCalledTimes(1);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should load the value and cache on error', async () => {
|
|
47
|
-
const error = new Error('fail');
|
|
48
|
-
const loader = jest.fn().mockRejectedValue(error);
|
|
49
|
-
|
|
50
|
-
const {result, rerender} = renderHook(
|
|
51
|
-
() => useLoader({
|
|
52
|
-
cacheKey: cacheKey.current(),
|
|
53
|
-
fallback: error,
|
|
54
|
-
loader: loader,
|
|
55
|
-
}),
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
rerender();
|
|
59
|
-
|
|
60
|
-
await waitFor(() => expect(result.current).toBe(error));
|
|
61
|
-
|
|
62
|
-
expect(loader).toHaveBeenCalledTimes(1);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should reload the value on error', async () => {
|
|
66
|
-
const content = {foo: 'qux'};
|
|
67
|
-
|
|
68
|
-
const loader = jest.fn()
|
|
69
|
-
.mockImplementationOnce(() => {
|
|
70
|
-
throw new Error('fail');
|
|
71
|
-
})
|
|
72
|
-
.mockImplementationOnce(() => Promise.resolve(content));
|
|
73
|
-
|
|
74
|
-
const {result, rerender} = renderHook(
|
|
75
|
-
() => useLoader({
|
|
76
|
-
cacheKey: cacheKey.current(),
|
|
77
|
-
initial: {},
|
|
78
|
-
loader: loader,
|
|
79
|
-
}),
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
await act(flushPromises);
|
|
83
|
-
|
|
84
|
-
rerender();
|
|
85
|
-
|
|
86
|
-
await waitFor(() => expect(result.current).toBe(content));
|
|
87
|
-
|
|
88
|
-
expect(loader).toHaveBeenCalledTimes(2);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should return the initial state on the initial render', async () => {
|
|
92
|
-
const loader = jest.fn(() => Promise.resolve('loaded'));
|
|
93
|
-
|
|
94
|
-
const {result} = renderHook(
|
|
95
|
-
() => useLoader({
|
|
96
|
-
cacheKey: cacheKey.current(),
|
|
97
|
-
initial: 'loading',
|
|
98
|
-
loader: loader,
|
|
99
|
-
}),
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
expect(result.current).toBe('loading');
|
|
103
|
-
|
|
104
|
-
await waitFor(() => expect(result.current).toBe('loaded'));
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should update the initial state with the fallback state on error', async () => {
|
|
108
|
-
const loader = jest.fn().mockRejectedValue(new Error('fail'));
|
|
109
|
-
|
|
110
|
-
const {result} = renderHook(
|
|
111
|
-
() => useLoader({
|
|
112
|
-
cacheKey: cacheKey.current(),
|
|
113
|
-
initial: 'loading',
|
|
114
|
-
fallback: 'error',
|
|
115
|
-
loader: loader,
|
|
116
|
-
}),
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
expect(result.current).toBe('loading');
|
|
120
|
-
|
|
121
|
-
await waitFor(() => expect(result.current).toBe('error'));
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('should return the fallback state on error', async () => {
|
|
125
|
-
const loader = jest.fn().mockRejectedValue(new Error('fail'));
|
|
126
|
-
|
|
127
|
-
const {result} = renderHook(
|
|
128
|
-
() => useLoader({
|
|
129
|
-
cacheKey: cacheKey.current(),
|
|
130
|
-
fallback: 'foo',
|
|
131
|
-
loader: loader,
|
|
132
|
-
}),
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
await waitFor(() => expect(result.current).toBe('foo'));
|
|
136
|
-
|
|
137
|
-
expect(loader).toHaveBeenCalled();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('should extend the cache expiration on every render', async () => {
|
|
141
|
-
jest.useFakeTimers();
|
|
142
|
-
|
|
143
|
-
const loader = jest.fn().mockResolvedValue('foo');
|
|
144
|
-
|
|
145
|
-
const {rerender, unmount} = renderHook(
|
|
146
|
-
() => useLoader({
|
|
147
|
-
cacheKey: cacheKey.current(),
|
|
148
|
-
loader: loader,
|
|
149
|
-
expiration: 15,
|
|
150
|
-
}),
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
await act(flushPromises);
|
|
154
|
-
|
|
155
|
-
jest.advanceTimersByTime(14);
|
|
156
|
-
|
|
157
|
-
rerender();
|
|
158
|
-
|
|
159
|
-
jest.advanceTimersByTime(14);
|
|
160
|
-
|
|
161
|
-
rerender();
|
|
162
|
-
|
|
163
|
-
expect(loader).toHaveBeenCalledTimes(1);
|
|
164
|
-
|
|
165
|
-
jest.advanceTimersByTime(15);
|
|
166
|
-
|
|
167
|
-
unmount();
|
|
168
|
-
|
|
169
|
-
renderHook(
|
|
170
|
-
() => useLoader({
|
|
171
|
-
cacheKey: cacheKey.current(),
|
|
172
|
-
loader: loader,
|
|
173
|
-
expiration: 15,
|
|
174
|
-
}),
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
await act(flushPromises);
|
|
178
|
-
|
|
179
|
-
expect(loader).toHaveBeenCalledTimes(2);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('should not expire the cache when the expiration is negative', async () => {
|
|
183
|
-
jest.useFakeTimers();
|
|
184
|
-
|
|
185
|
-
const loader = jest.fn(
|
|
186
|
-
() => new Promise(resolve => {
|
|
187
|
-
setTimeout(() => resolve('foo'), 10);
|
|
188
|
-
}),
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
const {rerender} = renderHook(
|
|
192
|
-
() => useLoader({
|
|
193
|
-
cacheKey: cacheKey.current(),
|
|
194
|
-
loader: loader,
|
|
195
|
-
expiration: -1,
|
|
196
|
-
}),
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
jest.advanceTimersByTime(10);
|
|
200
|
-
|
|
201
|
-
await act(flushPromises);
|
|
202
|
-
|
|
203
|
-
// First rerender
|
|
204
|
-
rerender();
|
|
205
|
-
|
|
206
|
-
// Second rerender
|
|
207
|
-
rerender();
|
|
208
|
-
|
|
209
|
-
expect(loader).toHaveBeenCalledTimes(1);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('should reload the value when the cache key changes without initial value', async () => {
|
|
213
|
-
jest.useFakeTimers();
|
|
214
|
-
|
|
215
|
-
const loader = jest.fn()
|
|
216
|
-
.mockResolvedValueOnce('foo')
|
|
217
|
-
.mockImplementationOnce(
|
|
218
|
-
() => new Promise(resolve => {
|
|
219
|
-
setTimeout(() => resolve('bar'), 10);
|
|
220
|
-
}),
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
const {result, rerender} = renderHook<any, {initial: string}>(
|
|
224
|
-
props => useLoader({
|
|
225
|
-
cacheKey: cacheKey.current(),
|
|
226
|
-
loader: loader,
|
|
227
|
-
initial: props?.initial,
|
|
228
|
-
}),
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
await act(flushPromises);
|
|
232
|
-
|
|
233
|
-
rerender();
|
|
234
|
-
|
|
235
|
-
await waitFor(() => expect(result.current).toBe('foo'));
|
|
236
|
-
|
|
237
|
-
expect(loader).toHaveBeenCalledTimes(1);
|
|
238
|
-
|
|
239
|
-
cacheKey.next();
|
|
240
|
-
|
|
241
|
-
rerender({initial: 'loading'});
|
|
242
|
-
|
|
243
|
-
await waitFor(() => expect(result.current).toBe('loading'));
|
|
244
|
-
|
|
245
|
-
jest.advanceTimersByTime(10);
|
|
246
|
-
|
|
247
|
-
await waitFor(() => expect(result.current).toBe('bar'));
|
|
248
|
-
|
|
249
|
-
expect(loader).toHaveBeenCalledTimes(2);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it('should reload the value when the cache key changes with initial value', async () => {
|
|
253
|
-
jest.useFakeTimers();
|
|
254
|
-
|
|
255
|
-
const loader = jest.fn()
|
|
256
|
-
.mockImplementationOnce(
|
|
257
|
-
() => new Promise(resolve => {
|
|
258
|
-
setTimeout(() => resolve('foo'), 10);
|
|
259
|
-
}),
|
|
260
|
-
)
|
|
261
|
-
.mockImplementationOnce(
|
|
262
|
-
() => new Promise(resolve => {
|
|
263
|
-
setTimeout(() => resolve('bar'), 10);
|
|
264
|
-
}),
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
const {result, rerender} = renderHook<any, {initial: string}>(
|
|
268
|
-
props => useLoader({
|
|
269
|
-
cacheKey: cacheKey.current(),
|
|
270
|
-
initial: props?.initial ?? 'first content',
|
|
271
|
-
loader: loader,
|
|
272
|
-
}),
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
await act(flushPromises);
|
|
276
|
-
|
|
277
|
-
expect(result.current).toBe('first content');
|
|
278
|
-
|
|
279
|
-
jest.advanceTimersByTime(10);
|
|
280
|
-
|
|
281
|
-
await act(flushPromises);
|
|
282
|
-
|
|
283
|
-
await waitFor(() => expect(result.current).toBe('foo'));
|
|
284
|
-
|
|
285
|
-
expect(loader).toHaveBeenCalledTimes(1);
|
|
286
|
-
|
|
287
|
-
cacheKey.next();
|
|
288
|
-
|
|
289
|
-
rerender({initial: 'second content'});
|
|
290
|
-
|
|
291
|
-
await waitFor(() => expect(result.current).toBe('second content'));
|
|
292
|
-
|
|
293
|
-
jest.advanceTimersByTime(10);
|
|
294
|
-
|
|
295
|
-
await act(flushPromises);
|
|
296
|
-
|
|
297
|
-
await waitFor(() => expect(result.current).toBe('bar'));
|
|
298
|
-
|
|
299
|
-
expect(loader).toHaveBeenCalledTimes(2);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it.each<[number, number|undefined]>(
|
|
303
|
-
[
|
|
304
|
-
// [Expected elapsed time, Expiration]
|
|
305
|
-
[60_000, undefined],
|
|
306
|
-
[15_000, 15_000],
|
|
307
|
-
],
|
|
308
|
-
)('should cache the values for %d milliseconds', async (step, expiration) => {
|
|
309
|
-
jest.useFakeTimers();
|
|
310
|
-
|
|
311
|
-
const delay = 10;
|
|
312
|
-
const loader = jest.fn(
|
|
313
|
-
() => new Promise(resolve => {
|
|
314
|
-
setTimeout(() => resolve('foo'), delay);
|
|
315
|
-
}),
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
const {result: firstTime} = renderHook(
|
|
319
|
-
() => useLoader({
|
|
320
|
-
cacheKey: cacheKey.current(),
|
|
321
|
-
expiration: expiration,
|
|
322
|
-
loader: loader,
|
|
323
|
-
}),
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
jest.advanceTimersByTime(delay);
|
|
327
|
-
|
|
328
|
-
await act(flushPromises);
|
|
329
|
-
|
|
330
|
-
await waitFor(() => expect(firstTime.current).toBe('foo'));
|
|
331
|
-
|
|
332
|
-
const {result: secondTime} = renderHook(
|
|
333
|
-
() => useLoader({
|
|
334
|
-
cacheKey: cacheKey.current(),
|
|
335
|
-
expiration: expiration,
|
|
336
|
-
loader: loader,
|
|
337
|
-
}),
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
expect(secondTime.current).toBe('foo');
|
|
341
|
-
|
|
342
|
-
expect(loader).toHaveBeenCalledTimes(1);
|
|
343
|
-
|
|
344
|
-
jest.advanceTimersByTime(step);
|
|
345
|
-
|
|
346
|
-
const {result: thirdTime} = renderHook(
|
|
347
|
-
() => useLoader({
|
|
348
|
-
cacheKey: cacheKey.current(),
|
|
349
|
-
expiration: expiration,
|
|
350
|
-
loader: loader,
|
|
351
|
-
}),
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
jest.advanceTimersByTime(delay);
|
|
355
|
-
|
|
356
|
-
await act(flushPromises);
|
|
357
|
-
|
|
358
|
-
await waitFor(() => expect(thirdTime.current).toBe('foo'));
|
|
359
|
-
|
|
360
|
-
expect(loader).toHaveBeenCalledTimes(2);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it('should dispose the cache on unmount', async () => {
|
|
364
|
-
jest.useFakeTimers();
|
|
365
|
-
|
|
366
|
-
const delay = 10;
|
|
367
|
-
const loader = jest.fn(
|
|
368
|
-
() => new Promise(resolve => {
|
|
369
|
-
setTimeout(() => resolve('foo'), delay);
|
|
370
|
-
}),
|
|
371
|
-
);
|
|
372
|
-
|
|
373
|
-
const {unmount} = renderHook(
|
|
374
|
-
() => useLoader({
|
|
375
|
-
cacheKey: cacheKey.current(),
|
|
376
|
-
expiration: 5,
|
|
377
|
-
loader: loader,
|
|
378
|
-
}),
|
|
379
|
-
);
|
|
380
|
-
|
|
381
|
-
jest.advanceTimersByTime(delay);
|
|
382
|
-
|
|
383
|
-
await act(flushPromises);
|
|
384
|
-
|
|
385
|
-
unmount();
|
|
386
|
-
|
|
387
|
-
jest.advanceTimersByTime(5);
|
|
388
|
-
|
|
389
|
-
await act(flushPromises);
|
|
390
|
-
|
|
391
|
-
const {result: secondTime} = renderHook(
|
|
392
|
-
() => useLoader({
|
|
393
|
-
cacheKey: cacheKey.current(),
|
|
394
|
-
expiration: 5,
|
|
395
|
-
loader: loader,
|
|
396
|
-
}),
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
jest.advanceTimersByTime(delay);
|
|
400
|
-
|
|
401
|
-
await act(flushPromises);
|
|
402
|
-
|
|
403
|
-
expect(loader).toHaveBeenCalledTimes(2);
|
|
404
|
-
|
|
405
|
-
await waitFor(() => expect(secondTime.current).toBe('foo'));
|
|
406
|
-
});
|
|
407
|
-
});
|
package/src/hooks/useLoader.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import {useCallback, useEffect, useRef, useState} from 'react';
|
|
2
|
-
import {Cache, EntryOptions} from './Cache';
|
|
3
|
-
|
|
4
|
-
const cache = new Cache(60 * 1000);
|
|
5
|
-
|
|
6
|
-
export type CacheOptions<R> = EntryOptions<R> & {
|
|
7
|
-
initial?: R,
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export function useLoader<R>({initial, ...options}: CacheOptions<R>): R {
|
|
11
|
-
const {cacheKey} = options;
|
|
12
|
-
const loadedValue: R|undefined = cache.get<R>(cacheKey)?.result;
|
|
13
|
-
const [value, setValue] = useState(loadedValue !== undefined ? loadedValue : initial);
|
|
14
|
-
const mountedRef = useRef(true);
|
|
15
|
-
const initialRef = useRef(initial);
|
|
16
|
-
const previousCacheKey = useRef(cacheKey);
|
|
17
|
-
|
|
18
|
-
const load = useStableCallback(() => {
|
|
19
|
-
try {
|
|
20
|
-
setValue(cache.load(options));
|
|
21
|
-
} catch (result: unknown) {
|
|
22
|
-
if (result instanceof Promise) {
|
|
23
|
-
result.then((resolvedValue: R) => {
|
|
24
|
-
if (mountedRef.current) {
|
|
25
|
-
setValue(resolvedValue);
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
setValue(undefined);
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const reset = useStableCallback(() => {
|
|
37
|
-
const newLoadedValue: R|undefined = cache.get<R>(cacheKey)?.result;
|
|
38
|
-
|
|
39
|
-
setValue(newLoadedValue !== undefined ? newLoadedValue : initial);
|
|
40
|
-
|
|
41
|
-
load();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
useEffect(
|
|
45
|
-
() => {
|
|
46
|
-
if (previousCacheKey.current !== cacheKey) {
|
|
47
|
-
reset();
|
|
48
|
-
previousCacheKey.current = cacheKey;
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
[reset, cacheKey],
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
useEffect(
|
|
55
|
-
() => {
|
|
56
|
-
if (initialRef.current !== undefined) {
|
|
57
|
-
load();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return () => {
|
|
61
|
-
mountedRef.current = false;
|
|
62
|
-
};
|
|
63
|
-
},
|
|
64
|
-
[load],
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
if (value === undefined) {
|
|
68
|
-
return cache.load(options);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return value;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
type Callback = () => void;
|
|
75
|
-
|
|
76
|
-
function useStableCallback(callback: Callback): Callback {
|
|
77
|
-
const ref = useRef<Callback>(undefined);
|
|
78
|
-
|
|
79
|
-
useEffect(() => {
|
|
80
|
-
ref.current = callback;
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
return useCallback(() => { ref.current?.(); }, []);
|
|
84
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* @jest-environment node
|
|
3
|
-
*/
|
|
4
|
-
import croct from '@croct/plug';
|
|
5
|
-
import {croct as croctPolyfill, isSsr} from './ssr-polyfills';
|
|
6
|
-
|
|
7
|
-
jest.mock(
|
|
8
|
-
'@croct/plug',
|
|
9
|
-
() => ({
|
|
10
|
-
plug: jest.fn(),
|
|
11
|
-
unplug: jest.fn(),
|
|
12
|
-
}),
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
describe('Croct polyfill (SSR)', () => {
|
|
16
|
-
it('should not plug', () => {
|
|
17
|
-
croctPolyfill.plug({appId: '00000000-0000-0000-0000-000000000000'});
|
|
18
|
-
|
|
19
|
-
expect(croct.plug).not.toHaveBeenCalled();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should not unplug', async () => {
|
|
23
|
-
await expect(croctPolyfill.unplug()).resolves.toBeUndefined();
|
|
24
|
-
|
|
25
|
-
expect(croct.unplug).not.toHaveBeenCalled();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should not initialize', () => {
|
|
29
|
-
expect(croctPolyfill.initialized).toBe(false);
|
|
30
|
-
|
|
31
|
-
croctPolyfill.plug({appId: '00000000-0000-0000-0000-000000000000'});
|
|
32
|
-
|
|
33
|
-
expect(croctPolyfill.initialized).toBe(false);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should not allow accessing properties other than plug or unplug', () => {
|
|
37
|
-
expect(() => croctPolyfill.user)
|
|
38
|
-
.toThrow('Property croct.user is not supported on server-side (SSR).');
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('isSsr', () => {
|
|
43
|
-
it('should always return true', () => {
|
|
44
|
-
expect(isSsr()).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import croct, {Configuration} from '@croct/plug';
|
|
2
|
-
import {croct as croctPolyfill, isSsr} from './ssr-polyfills';
|
|
3
|
-
import spyOn = jest.spyOn;
|
|
4
|
-
|
|
5
|
-
describe('Croct polyfill (CSR)', () => {
|
|
6
|
-
beforeAll(() => {
|
|
7
|
-
jest.clearAllMocks();
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
const config: Configuration = {appId: '00000000-0000-0000-0000-000000000000'};
|
|
11
|
-
|
|
12
|
-
it('should delay unplugging to avoid reconnections', async () => {
|
|
13
|
-
jest.useFakeTimers();
|
|
14
|
-
|
|
15
|
-
const unplug = spyOn(croct, 'unplug');
|
|
16
|
-
|
|
17
|
-
const plug = spyOn(croct, 'plug');
|
|
18
|
-
|
|
19
|
-
croctPolyfill.plug(config);
|
|
20
|
-
|
|
21
|
-
expect(plug).toHaveBeenCalledTimes(1);
|
|
22
|
-
|
|
23
|
-
// First attempt: cancelling
|
|
24
|
-
|
|
25
|
-
const firstAttempt = croctPolyfill.unplug();
|
|
26
|
-
|
|
27
|
-
expect(unplug).not.toHaveBeenCalled();
|
|
28
|
-
|
|
29
|
-
croctPolyfill.plug(config);
|
|
30
|
-
|
|
31
|
-
jest.runOnlyPendingTimers();
|
|
32
|
-
|
|
33
|
-
await expect(firstAttempt).rejects.toThrow('Unplug cancelled.');
|
|
34
|
-
|
|
35
|
-
expect(unplug).not.toHaveBeenCalled();
|
|
36
|
-
|
|
37
|
-
// Second attempt: failing
|
|
38
|
-
|
|
39
|
-
unplug.mockRejectedValueOnce(new Error('Unplug failed.'));
|
|
40
|
-
|
|
41
|
-
const secondAttempt = croct.unplug();
|
|
42
|
-
|
|
43
|
-
jest.runOnlyPendingTimers();
|
|
44
|
-
|
|
45
|
-
await expect(secondAttempt).rejects.toThrow('Unplug failed.');
|
|
46
|
-
|
|
47
|
-
// Third attempt: succeeding
|
|
48
|
-
|
|
49
|
-
unplug.mockResolvedValueOnce();
|
|
50
|
-
|
|
51
|
-
const thirdAttempt = croct.unplug();
|
|
52
|
-
|
|
53
|
-
jest.runOnlyPendingTimers();
|
|
54
|
-
|
|
55
|
-
await expect(thirdAttempt).resolves.toBeUndefined();
|
|
56
|
-
|
|
57
|
-
expect(unplug).toHaveBeenCalledTimes(2);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('isSsr', () => {
|
|
62
|
-
it('should always return false', () => {
|
|
63
|
-
expect(isSsr()).toBe(false);
|
|
64
|
-
});
|
|
65
|
-
});
|