@data-client/core 0.14.16 → 0.14.19
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/dist/index.js +556 -280
- package/dist/index.umd.min.js +1 -1
- package/legacy/actions.js +1 -1
- package/legacy/controller/Controller.js +101 -12
- package/legacy/controller/actions/createFetch.js +3 -2
- package/legacy/controller/actions/createSubscription.js +3 -3
- package/legacy/controller/ensurePojo.js +3 -2
- package/legacy/index.js +3 -4
- package/legacy/manager/DevtoolsManager.js +14 -9
- package/legacy/manager/NetworkManager.js +8 -5
- package/legacy/manager/PollingSubscription.js +6 -3
- package/legacy/manager/SubscriptionManager.js +4 -3
- package/legacy/manager/applyManager.js +3 -7
- package/legacy/manager/initManager.js +15 -0
- package/legacy/state/GCPolicy.js +153 -0
- package/legacy/state/reducer/createReducer.js +7 -3
- package/legacy/state/reducer/expireReducer.js +5 -4
- package/legacy/state/reducer/fetchReducer.js +3 -2
- package/legacy/state/reducer/invalidateReducer.js +6 -5
- package/legacy/state/reducer/setResponseReducer.js +10 -7
- package/legacy/types.js +1 -1
- package/lib/actionTypes.d.ts.map +1 -1
- package/lib/actions.d.ts +2 -2
- package/lib/actions.d.ts.map +1 -1
- package/lib/actions.js +1 -1
- package/lib/controller/Controller.d.ts +108 -5
- package/lib/controller/Controller.d.ts.map +1 -1
- package/lib/controller/Controller.js +97 -9
- package/lib/controller/actions/createSubscription.js +3 -3
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -4
- package/lib/manager/DevtoolsManager.d.ts.map +1 -1
- package/lib/manager/DevtoolsManager.js +7 -5
- package/lib/manager/NetworkManager.js +3 -2
- package/lib/manager/PollingSubscription.js +1 -1
- package/lib/manager/SubscriptionManager.d.ts.map +1 -1
- package/lib/manager/SubscriptionManager.js +1 -1
- package/lib/manager/applyManager.d.ts.map +1 -1
- package/lib/manager/applyManager.js +3 -7
- package/lib/manager/initManager.d.ts +4 -0
- package/lib/manager/initManager.d.ts.map +1 -0
- package/lib/manager/initManager.js +15 -0
- package/lib/state/GCPolicy.d.ts +55 -0
- package/lib/state/GCPolicy.d.ts.map +1 -0
- package/lib/state/GCPolicy.js +153 -0
- package/lib/state/reducer/createReducer.js +5 -2
- package/lib/state/reducer/expireReducer.d.ts +1 -0
- package/lib/state/reducer/expireReducer.d.ts.map +1 -1
- package/lib/state/reducer/invalidateReducer.d.ts +1 -0
- package/lib/state/reducer/invalidateReducer.d.ts.map +1 -1
- package/lib/state/reducer/setReducer.d.ts +5 -4
- package/lib/state/reducer/setReducer.d.ts.map +1 -1
- package/lib/state/reducer/setResponseReducer.d.ts +6 -4
- package/lib/state/reducer/setResponseReducer.d.ts.map +1 -1
- package/lib/state/reducer/setResponseReducer.js +4 -2
- package/lib/types.d.ts +1 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +1 -1
- package/package.json +12 -4
- package/src/actions.ts +2 -1
- package/src/controller/Controller.ts +206 -9
- package/src/controller/__tests__/Controller.ts +8 -4
- package/src/controller/__tests__/__snapshots__/get.ts.snap +7 -0
- package/src/controller/__tests__/__snapshots__/getResponse.ts.snap +15 -0
- package/src/controller/__tests__/get.ts +45 -17
- package/src/controller/__tests__/getResponse.ts +46 -0
- package/src/controller/actions/createSubscription.ts +2 -2
- package/src/index.ts +2 -6
- package/src/manager/DevtoolsManager.ts +2 -3
- package/src/manager/PollingSubscription.ts +9 -9
- package/src/manager/SubscriptionManager.ts +1 -2
- package/src/manager/applyManager.ts +3 -4
- package/src/manager/initManager.ts +21 -0
- package/src/state/GCPolicy.ts +197 -0
- package/src/state/__tests__/GCPolicy.test.ts +258 -0
- package/src/state/__tests__/__snapshots__/reducer.ts.snap +2 -0
- package/src/state/__tests__/reducer.ts +4 -4
- package/src/state/reducer/createReducer.ts +1 -1
- package/src/state/reducer/setResponseReducer.ts +3 -1
- package/src/types.ts +1 -0
- package/ts3.4/actions.d.ts +2 -5
- package/ts3.4/controller/Controller.d.ts +141 -5
- package/ts3.4/index.d.ts +2 -0
- package/ts3.4/manager/initManager.d.ts +4 -0
- package/ts3.4/state/GCPolicy.d.ts +55 -0
- package/ts3.4/state/reducer/expireReducer.d.ts +1 -0
- package/ts3.4/state/reducer/invalidateReducer.d.ts +1 -0
- package/ts3.4/state/reducer/setReducer.d.ts +3 -2
- package/ts3.4/state/reducer/setResponseReducer.d.ts +4 -2
- package/ts3.4/types.d.ts +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type Controller from '../controller/Controller.js';
|
|
2
|
+
import { Manager, State } from '../types.js';
|
|
3
|
+
|
|
4
|
+
export default function initManager(
|
|
5
|
+
managers: Manager[],
|
|
6
|
+
controller: Controller,
|
|
7
|
+
initialState: State<unknown>,
|
|
8
|
+
) {
|
|
9
|
+
return () => {
|
|
10
|
+
managers.forEach(manager => {
|
|
11
|
+
manager.init?.(initialState);
|
|
12
|
+
});
|
|
13
|
+
controller.gcPolicy.init(controller);
|
|
14
|
+
return () => {
|
|
15
|
+
managers.forEach(manager => {
|
|
16
|
+
manager.cleanup();
|
|
17
|
+
});
|
|
18
|
+
controller.gcPolicy.cleanup();
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { EntityPath } from '@data-client/normalizr';
|
|
2
|
+
|
|
3
|
+
import { GC } from '../actionTypes.js';
|
|
4
|
+
import Controller from '../controller/Controller.js';
|
|
5
|
+
|
|
6
|
+
export class GCPolicy implements GCInterface {
|
|
7
|
+
protected endpointCount = new Map<string, number>();
|
|
8
|
+
protected entityCount = new Map<string, Map<string, number>>();
|
|
9
|
+
protected endpointsQ = new Set<string>();
|
|
10
|
+
protected entitiesQ: EntityPath[] = [];
|
|
11
|
+
|
|
12
|
+
declare protected intervalId: ReturnType<typeof setInterval>;
|
|
13
|
+
declare protected controller: Controller;
|
|
14
|
+
declare protected options: Required<Omit<GCOptions, 'expiresAt'>>;
|
|
15
|
+
|
|
16
|
+
constructor({
|
|
17
|
+
// every 5 min
|
|
18
|
+
intervalMS = 60 * 1000 * 5,
|
|
19
|
+
expiryMultiplier = 2,
|
|
20
|
+
expiresAt,
|
|
21
|
+
}: GCOptions = {}) {
|
|
22
|
+
if (expiresAt) {
|
|
23
|
+
this.expiresAt = expiresAt.bind(this);
|
|
24
|
+
}
|
|
25
|
+
this.options = {
|
|
26
|
+
intervalMS,
|
|
27
|
+
expiryMultiplier,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
init(controller: Controller) {
|
|
32
|
+
this.controller = controller;
|
|
33
|
+
|
|
34
|
+
this.intervalId = setInterval(() => {
|
|
35
|
+
this.idleCallback(() => this.runSweep(), { timeout: 1000 });
|
|
36
|
+
}, this.options.intervalMS);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
cleanup() {
|
|
40
|
+
clearInterval(this.intervalId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
createCountRef({ key, paths = [] }: { key?: string; paths?: EntityPath[] }) {
|
|
44
|
+
// increment
|
|
45
|
+
return () => {
|
|
46
|
+
if (key)
|
|
47
|
+
this.endpointCount.set(key, (this.endpointCount.get(key) ?? 0) + 1);
|
|
48
|
+
paths.forEach(path => {
|
|
49
|
+
if (!this.entityCount.has(path.key)) {
|
|
50
|
+
this.entityCount.set(path.key, new Map<string, number>());
|
|
51
|
+
}
|
|
52
|
+
const instanceCount = this.entityCount.get(path.key)!;
|
|
53
|
+
instanceCount.set(path.pk, (instanceCount.get(path.pk) ?? 0) + 1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// decrement
|
|
57
|
+
return () => {
|
|
58
|
+
if (key) {
|
|
59
|
+
const currentCount = this.endpointCount.get(key)!;
|
|
60
|
+
if (currentCount !== undefined) {
|
|
61
|
+
if (currentCount <= 1) {
|
|
62
|
+
this.endpointCount.delete(key);
|
|
63
|
+
// queue for cleanup
|
|
64
|
+
this.endpointsQ.add(key);
|
|
65
|
+
} else {
|
|
66
|
+
this.endpointCount.set(key, currentCount - 1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
paths.forEach(path => {
|
|
71
|
+
if (!this.entityCount.has(path.key)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const instanceCount = this.entityCount.get(path.key)!;
|
|
75
|
+
const entityCount = instanceCount.get(path.pk)!;
|
|
76
|
+
if (entityCount !== undefined) {
|
|
77
|
+
if (entityCount <= 1) {
|
|
78
|
+
instanceCount.delete(path.pk);
|
|
79
|
+
// queue for cleanup
|
|
80
|
+
this.entitiesQ.push(path);
|
|
81
|
+
} else {
|
|
82
|
+
instanceCount.set(path.pk, entityCount - 1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
protected expiresAt({
|
|
91
|
+
fetchedAt,
|
|
92
|
+
expiresAt,
|
|
93
|
+
}: {
|
|
94
|
+
expiresAt: number;
|
|
95
|
+
date: number;
|
|
96
|
+
fetchedAt: number;
|
|
97
|
+
}): number {
|
|
98
|
+
return (
|
|
99
|
+
Math.max(
|
|
100
|
+
(expiresAt - fetchedAt) * this.options.expiryMultiplier,
|
|
101
|
+
120000,
|
|
102
|
+
) + fetchedAt
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
protected runSweep() {
|
|
107
|
+
const state = this.controller.getState();
|
|
108
|
+
const entities: EntityPath[] = [];
|
|
109
|
+
const endpoints: string[] = [];
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
|
|
112
|
+
const nextEndpointsQ = new Set<string>();
|
|
113
|
+
for (const key of this.endpointsQ) {
|
|
114
|
+
if (
|
|
115
|
+
!this.endpointCount.has(key) &&
|
|
116
|
+
this.expiresAt(
|
|
117
|
+
state.meta[key] ?? {
|
|
118
|
+
fetchedAt: 0,
|
|
119
|
+
date: 0,
|
|
120
|
+
expiresAt: 0,
|
|
121
|
+
},
|
|
122
|
+
) < now
|
|
123
|
+
) {
|
|
124
|
+
endpoints.push(key);
|
|
125
|
+
} else {
|
|
126
|
+
nextEndpointsQ.add(key);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
this.endpointsQ = nextEndpointsQ;
|
|
130
|
+
|
|
131
|
+
const nextEntitiesQ: EntityPath[] = [];
|
|
132
|
+
for (const path of this.entitiesQ) {
|
|
133
|
+
if (
|
|
134
|
+
!this.entityCount.get(path.key)?.has(path.pk) &&
|
|
135
|
+
this.expiresAt(
|
|
136
|
+
state.entityMeta[path.key]?.[path.pk] ?? {
|
|
137
|
+
fetchedAt: 0,
|
|
138
|
+
date: 0,
|
|
139
|
+
expiresAt: 0,
|
|
140
|
+
},
|
|
141
|
+
) < now
|
|
142
|
+
) {
|
|
143
|
+
entities.push(path);
|
|
144
|
+
} else {
|
|
145
|
+
nextEntitiesQ.push(path);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
this.entitiesQ = nextEntitiesQ;
|
|
149
|
+
|
|
150
|
+
if (entities.length || endpoints.length) {
|
|
151
|
+
this.controller.dispatch({ type: GC, entities, endpoints });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Calls the callback when client is not 'busy' with high priority interaction tasks
|
|
156
|
+
*
|
|
157
|
+
* Override for platform-specific implementations
|
|
158
|
+
*/
|
|
159
|
+
protected idleCallback(
|
|
160
|
+
callback: (...args: any[]) => void,
|
|
161
|
+
options?: IdleRequestOptions,
|
|
162
|
+
) {
|
|
163
|
+
if (typeof requestIdleCallback === 'function') {
|
|
164
|
+
requestIdleCallback(callback, options);
|
|
165
|
+
} else {
|
|
166
|
+
callback();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export class ImmortalGCPolicy implements GCInterface {
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
173
|
+
init() {}
|
|
174
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
175
|
+
cleanup() {}
|
|
176
|
+
createCountRef() {
|
|
177
|
+
return () => () => undefined;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface GCOptions {
|
|
182
|
+
intervalMS?: number;
|
|
183
|
+
expiryMultiplier?: number;
|
|
184
|
+
expiresAt?: (meta: {
|
|
185
|
+
expiresAt: number;
|
|
186
|
+
date: number;
|
|
187
|
+
fetchedAt: number;
|
|
188
|
+
}) => number;
|
|
189
|
+
}
|
|
190
|
+
export interface CreateCountRef {
|
|
191
|
+
({ key, paths }: { key?: string; paths?: EntityPath[] }): () => () => void;
|
|
192
|
+
}
|
|
193
|
+
export interface GCInterface {
|
|
194
|
+
createCountRef: CreateCountRef;
|
|
195
|
+
init(controller: Controller): void;
|
|
196
|
+
cleanup(): void;
|
|
197
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { EntityPath } from '@data-client/normalizr';
|
|
2
|
+
import { jest } from '@jest/globals';
|
|
3
|
+
import { has } from 'benchmark';
|
|
4
|
+
|
|
5
|
+
import { GC } from '../../actionTypes';
|
|
6
|
+
import Controller from '../../controller/Controller';
|
|
7
|
+
import { GCPolicy } from '../GCPolicy';
|
|
8
|
+
|
|
9
|
+
describe('GCPolicy', () => {
|
|
10
|
+
let gcPolicy: GCPolicy;
|
|
11
|
+
let controller: Controller;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
controller = {
|
|
15
|
+
getState: jest.fn(),
|
|
16
|
+
dispatch: jest.fn(),
|
|
17
|
+
} as unknown as Controller;
|
|
18
|
+
gcPolicy = new GCPolicy();
|
|
19
|
+
gcPolicy.init(controller);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
gcPolicy.cleanup();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should increment and decrement endpoint and entity counts', () => {
|
|
27
|
+
const key = 'testEndpoint';
|
|
28
|
+
const paths: EntityPath[] = [{ key: 'testEntity', pk: '1' }];
|
|
29
|
+
const countRef = gcPolicy.createCountRef({ key, paths });
|
|
30
|
+
|
|
31
|
+
const decrement = countRef();
|
|
32
|
+
expect(gcPolicy['endpointCount'].get(key)).toBe(1);
|
|
33
|
+
expect(gcPolicy['entityCount'].get('testEntity')?.get('1')).toBe(1);
|
|
34
|
+
|
|
35
|
+
decrement();
|
|
36
|
+
expect(gcPolicy['endpointCount'].get(key)).toBeUndefined();
|
|
37
|
+
expect(gcPolicy['entityCount'].get('testEntity')?.get('1')).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should dispatch GC action once no ref counts and is expired', () => {
|
|
41
|
+
const key = 'testEndpoint';
|
|
42
|
+
const paths: EntityPath[] = [{ key: 'testEntity', pk: '1' }];
|
|
43
|
+
const state = {
|
|
44
|
+
meta: {
|
|
45
|
+
testEndpoint: {
|
|
46
|
+
date: 0,
|
|
47
|
+
fetchedAt: 0,
|
|
48
|
+
expiresAt: 0,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
entityMeta: {
|
|
52
|
+
testEntity: {
|
|
53
|
+
'1': {
|
|
54
|
+
date: 0,
|
|
55
|
+
fetchedAt: 0,
|
|
56
|
+
expiresAt: 0,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
(controller.getState as jest.Mock).mockReturnValue(state);
|
|
62
|
+
|
|
63
|
+
const countRef = gcPolicy.createCountRef({ key, paths });
|
|
64
|
+
|
|
65
|
+
const decrement = countRef();
|
|
66
|
+
countRef(); // Increment again
|
|
67
|
+
gcPolicy['runSweep']();
|
|
68
|
+
expect(controller.dispatch).not.toHaveBeenCalled();
|
|
69
|
+
decrement();
|
|
70
|
+
gcPolicy['runSweep']();
|
|
71
|
+
expect(controller.dispatch).not.toHaveBeenCalled();
|
|
72
|
+
decrement(); // Decrement twice
|
|
73
|
+
|
|
74
|
+
gcPolicy['runSweep']();
|
|
75
|
+
expect(controller.dispatch).toHaveBeenCalledWith({
|
|
76
|
+
type: GC,
|
|
77
|
+
entities: [{ key: 'testEntity', pk: '1' }],
|
|
78
|
+
endpoints: ['testEndpoint'],
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should dispatch GC action once no ref counts and is expired with extra decrements', () => {
|
|
83
|
+
const key = 'testEndpoint';
|
|
84
|
+
const paths: EntityPath[] = [{ key: 'testEntity', pk: '1' }];
|
|
85
|
+
const state = {
|
|
86
|
+
meta: {
|
|
87
|
+
testEndpoint: {
|
|
88
|
+
date: 0,
|
|
89
|
+
fetchedAt: 0,
|
|
90
|
+
expiresAt: 0,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
entityMeta: {
|
|
94
|
+
testEntity: {
|
|
95
|
+
'1': {
|
|
96
|
+
date: 0,
|
|
97
|
+
fetchedAt: 0,
|
|
98
|
+
expiresAt: 0,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
(controller.getState as jest.Mock).mockReturnValue(state);
|
|
104
|
+
|
|
105
|
+
const countRef = gcPolicy.createCountRef({ key, paths });
|
|
106
|
+
|
|
107
|
+
const decrement = countRef();
|
|
108
|
+
countRef(); // Increment again
|
|
109
|
+
gcPolicy['runSweep']();
|
|
110
|
+
expect(controller.dispatch).not.toHaveBeenCalled();
|
|
111
|
+
decrement();
|
|
112
|
+
gcPolicy['runSweep']();
|
|
113
|
+
expect(controller.dispatch).not.toHaveBeenCalled();
|
|
114
|
+
decrement(); // Decrement twice
|
|
115
|
+
decrement(); // Decrement extra time
|
|
116
|
+
|
|
117
|
+
gcPolicy['runSweep']();
|
|
118
|
+
expect(controller.dispatch).toHaveBeenCalledWith({
|
|
119
|
+
type: GC,
|
|
120
|
+
entities: [{ key: 'testEntity', pk: '1' }],
|
|
121
|
+
endpoints: ['testEndpoint'],
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should dispatch GC action once no ref counts and no expiry state', () => {
|
|
126
|
+
const key = 'testEndpoint';
|
|
127
|
+
const paths: EntityPath[] = [{ key: 'testEntity', pk: '1' }];
|
|
128
|
+
const state = {
|
|
129
|
+
meta: {},
|
|
130
|
+
entityMeta: {},
|
|
131
|
+
};
|
|
132
|
+
(controller.getState as jest.Mock).mockReturnValue(state);
|
|
133
|
+
|
|
134
|
+
const countRef = gcPolicy.createCountRef({ key, paths });
|
|
135
|
+
|
|
136
|
+
const decrement = countRef();
|
|
137
|
+
countRef(); // Increment again
|
|
138
|
+
gcPolicy['runSweep']();
|
|
139
|
+
expect(controller.dispatch).not.toHaveBeenCalled();
|
|
140
|
+
decrement();
|
|
141
|
+
gcPolicy['runSweep']();
|
|
142
|
+
expect(controller.dispatch).not.toHaveBeenCalled();
|
|
143
|
+
decrement(); // Decrement twice
|
|
144
|
+
|
|
145
|
+
gcPolicy['runSweep']();
|
|
146
|
+
expect(controller.dispatch).toHaveBeenCalledWith({
|
|
147
|
+
type: GC,
|
|
148
|
+
entities: [{ key: 'testEntity', pk: '1' }],
|
|
149
|
+
endpoints: ['testEndpoint'],
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should not dispatch GC action if expiresAt has not passed, but dispatch later when it has', () => {
|
|
154
|
+
jest.useFakeTimers();
|
|
155
|
+
const key = 'testEndpoint';
|
|
156
|
+
const paths: EntityPath[] = [{ key: 'testEntity', pk: '1' }];
|
|
157
|
+
const futureTime = Date.now() + 1000;
|
|
158
|
+
const state = {
|
|
159
|
+
meta: {
|
|
160
|
+
testEndpoint: {
|
|
161
|
+
date: futureTime - 100,
|
|
162
|
+
fetchAt: futureTime - 100,
|
|
163
|
+
expiresAt: futureTime,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
entityMeta: {
|
|
167
|
+
testEntity: {
|
|
168
|
+
'1': {
|
|
169
|
+
date: futureTime - 100,
|
|
170
|
+
fetchAt: futureTime - 100,
|
|
171
|
+
expiresAt: futureTime,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
(controller.getState as jest.Mock).mockReturnValue(state);
|
|
177
|
+
|
|
178
|
+
const countRef = gcPolicy.createCountRef({ key, paths });
|
|
179
|
+
|
|
180
|
+
const decrement = countRef();
|
|
181
|
+
countRef(); // Increment again
|
|
182
|
+
decrement();
|
|
183
|
+
decrement(); // Decrement twice
|
|
184
|
+
|
|
185
|
+
gcPolicy['runSweep']();
|
|
186
|
+
|
|
187
|
+
expect(controller.dispatch).not.toHaveBeenCalled();
|
|
188
|
+
|
|
189
|
+
// Fast forward time to past the futureTime
|
|
190
|
+
jest.advanceTimersByTime(2000);
|
|
191
|
+
(controller.getState as jest.Mock).mockReturnValue({
|
|
192
|
+
meta: {
|
|
193
|
+
testEndpoint: {
|
|
194
|
+
date: 0,
|
|
195
|
+
fetchedAt: 0,
|
|
196
|
+
expiresAt: 0,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
entityMeta: {
|
|
200
|
+
testEntity: {
|
|
201
|
+
'1': {
|
|
202
|
+
date: 0,
|
|
203
|
+
fetchedAt: 0,
|
|
204
|
+
expiresAt: 0,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
gcPolicy['runSweep']();
|
|
211
|
+
|
|
212
|
+
expect(controller.dispatch).toHaveBeenCalledWith({
|
|
213
|
+
type: GC,
|
|
214
|
+
entities: [{ key: 'testEntity', pk: '1' }],
|
|
215
|
+
endpoints: ['testEndpoint'],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
jest.useRealTimers();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should support custom hasExpired', () => {
|
|
222
|
+
jest.useFakeTimers();
|
|
223
|
+
gcPolicy = new GCPolicy({ expiresAt: () => 0 });
|
|
224
|
+
gcPolicy.init(controller);
|
|
225
|
+
const key = 'testEndpoint';
|
|
226
|
+
const paths: EntityPath[] = [{ key: 'testEntity', pk: '1' }];
|
|
227
|
+
const futureTime = Date.now() + 1000;
|
|
228
|
+
const state = {
|
|
229
|
+
meta: {
|
|
230
|
+
testEndpoint: {
|
|
231
|
+
date: futureTime - 100,
|
|
232
|
+
fetchAt: futureTime - 100,
|
|
233
|
+
expiresAt: futureTime,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
entityMeta: {
|
|
237
|
+
testEntity: {
|
|
238
|
+
'1': {
|
|
239
|
+
date: futureTime - 100,
|
|
240
|
+
fetchAt: futureTime - 100,
|
|
241
|
+
expiresAt: futureTime,
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
(controller.getState as jest.Mock).mockReturnValue(state);
|
|
247
|
+
|
|
248
|
+
const countRef = gcPolicy.createCountRef({ key, paths });
|
|
249
|
+
|
|
250
|
+
const decrement = countRef();
|
|
251
|
+
countRef(); // Increment again
|
|
252
|
+
decrement();
|
|
253
|
+
decrement(); // Decrement twice
|
|
254
|
+
|
|
255
|
+
gcPolicy['runSweep']();
|
|
256
|
+
expect(controller.dispatch).toHaveBeenCalled();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -13,6 +13,7 @@ exports[`reducer should set error in meta for "set" 1`] = `
|
|
|
13
13
|
"error": [Error: hi],
|
|
14
14
|
"errorPolicy": undefined,
|
|
15
15
|
"expiresAt": 5000500000,
|
|
16
|
+
"fetchedAt": 5000000000,
|
|
16
17
|
},
|
|
17
18
|
},
|
|
18
19
|
"optimistic": [],
|
|
@@ -48,6 +49,7 @@ exports[`reducer singles should update state correctly 1`] = `
|
|
|
48
49
|
"http://test.com/article/20": {
|
|
49
50
|
"date": 5000000000,
|
|
50
51
|
"expiresAt": 5000500000,
|
|
52
|
+
"fetchedAt": 5000000000,
|
|
51
53
|
"prevExpiresAt": undefined,
|
|
52
54
|
},
|
|
53
55
|
},
|
|
@@ -712,8 +712,8 @@ describe('reducer', () => {
|
|
|
712
712
|
const action: GCAction = {
|
|
713
713
|
type: GC,
|
|
714
714
|
entities: [
|
|
715
|
-
|
|
716
|
-
|
|
715
|
+
{ key: Article.key, pk: '10' },
|
|
716
|
+
{ key: Article.key, pk: '250' },
|
|
717
717
|
],
|
|
718
718
|
endpoints: ['abc'],
|
|
719
719
|
};
|
|
@@ -731,8 +731,8 @@ describe('reducer', () => {
|
|
|
731
731
|
const action: GCAction = {
|
|
732
732
|
type: GC,
|
|
733
733
|
entities: [
|
|
734
|
-
|
|
735
|
-
|
|
734
|
+
{ key: Article.key, pk: '100000000' },
|
|
735
|
+
{ key: 'sillythings', pk: '10' },
|
|
736
736
|
],
|
|
737
737
|
endpoints: [],
|
|
738
738
|
};
|
|
@@ -26,7 +26,7 @@ export default function createReducer(controller: Controller): ReducerType {
|
|
|
26
26
|
switch (action.type) {
|
|
27
27
|
case GC:
|
|
28
28
|
// inline deletes are fine as these should have 0 refcounts
|
|
29
|
-
action.entities.forEach((
|
|
29
|
+
action.entities.forEach(({ key, pk }) => {
|
|
30
30
|
delete (state as any).entities[key]?.[pk];
|
|
31
31
|
delete (state as any).entityMeta[key]?.[pk];
|
|
32
32
|
});
|
|
@@ -75,6 +75,7 @@ export function setResponseReducer(
|
|
|
75
75
|
...state.meta,
|
|
76
76
|
[action.key]: {
|
|
77
77
|
date: action.meta.date,
|
|
78
|
+
fetchedAt: action.meta.fetchedAt,
|
|
78
79
|
expiresAt: action.meta.expiresAt,
|
|
79
80
|
prevExpiresAt: state.meta[action.key]?.expiresAt,
|
|
80
81
|
},
|
|
@@ -126,8 +127,9 @@ function reduceError(
|
|
|
126
127
|
...state.meta,
|
|
127
128
|
[action.key]: {
|
|
128
129
|
date: action.meta.date,
|
|
129
|
-
|
|
130
|
+
fetchedAt: action.meta.fetchedAt,
|
|
130
131
|
expiresAt: action.meta.expiresAt,
|
|
132
|
+
error,
|
|
131
133
|
errorPolicy: action.endpoint.errorPolicy?.(error),
|
|
132
134
|
},
|
|
133
135
|
},
|
package/src/types.ts
CHANGED
package/ts3.4/actions.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Denormalize, EndpointInterface, Queryable, ResolveType, UnknownError } from '@data-client/normalizr';
|
|
1
|
+
import { Denormalize, EndpointInterface, EntityPath, Queryable, ResolveType, UnknownError } from '@data-client/normalizr';
|
|
2
2
|
import { SET, RESET, FETCH, SUBSCRIBE, UNSUBSCRIBE, INVALIDATE, GC, OPTIMISTIC, INVALIDATEALL, EXPIREALL, SET_RESPONSE } from './actionTypes.js';
|
|
3
3
|
import { EndpointUpdateFunction } from './controller/types.js';
|
|
4
4
|
type EndpointAndUpdate<E extends EndpointInterface> = EndpointInterface & {
|
|
@@ -95,10 +95,7 @@ export interface ResetAction {
|
|
|
95
95
|
}
|
|
96
96
|
export interface GCAction {
|
|
97
97
|
type: typeof GC;
|
|
98
|
-
entities: [
|
|
99
|
-
string,
|
|
100
|
-
string
|
|
101
|
-
][];
|
|
98
|
+
entities: EntityPath[];
|
|
102
99
|
endpoints: string[];
|
|
103
100
|
}
|
|
104
101
|
/** @see https://dataclient.io/docs/api/Actions */
|