@data-client/core 0.12.3 → 0.13.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.
Files changed (92) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +89 -15
  3. package/dist/index.umd.min.js +1 -1
  4. package/legacy/actionTypes.js +2 -1
  5. package/legacy/actions.js +1 -1
  6. package/legacy/controller/Controller.js +21 -7
  7. package/legacy/controller/createSet.js +7 -11
  8. package/legacy/controller/createSetResponse.js +32 -0
  9. package/legacy/index.js +2 -1
  10. package/legacy/manager/DevtoolsManager.js +5 -5
  11. package/legacy/manager/LogoutManager.js +3 -3
  12. package/legacy/manager/NetworkManager.js +5 -5
  13. package/legacy/manager/PollingSubscription.js +1 -1
  14. package/legacy/manager/SubscriptionManager.js +1 -1
  15. package/legacy/next/index.js +1 -1
  16. package/legacy/state/reducer/createReducer.js +6 -3
  17. package/legacy/state/reducer/fetchReducer.js +1 -1
  18. package/legacy/state/reducer/setReducer.js +7 -85
  19. package/legacy/state/reducer/setResponseReducer.js +107 -0
  20. package/legacy/types.js +1 -1
  21. package/lib/actionTypes.d.ts +1 -0
  22. package/lib/actionTypes.d.ts.map +1 -1
  23. package/lib/actionTypes.js +2 -1
  24. package/lib/actions.d.ts +23 -11
  25. package/lib/actions.d.ts.map +1 -1
  26. package/lib/actions.js +1 -1
  27. package/lib/controller/Controller.d.ts +7 -2
  28. package/lib/controller/Controller.d.ts.map +1 -1
  29. package/lib/controller/Controller.js +21 -7
  30. package/lib/controller/createSet.d.ts +5 -17
  31. package/lib/controller/createSet.d.ts.map +1 -1
  32. package/lib/controller/createSet.js +7 -11
  33. package/lib/controller/createSetResponse.d.ts +20 -0
  34. package/lib/controller/createSetResponse.d.ts.map +1 -0
  35. package/lib/controller/createSetResponse.js +32 -0
  36. package/lib/index.d.ts +1 -0
  37. package/lib/index.d.ts.map +1 -1
  38. package/lib/index.js +2 -1
  39. package/lib/manager/DevtoolsManager.d.ts.map +1 -1
  40. package/lib/manager/DevtoolsManager.js +5 -3
  41. package/lib/manager/LogoutManager.js +3 -3
  42. package/lib/manager/NetworkManager.d.ts +2 -2
  43. package/lib/manager/NetworkManager.d.ts.map +1 -1
  44. package/lib/manager/NetworkManager.js +5 -5
  45. package/lib/manager/PollingSubscription.js +1 -1
  46. package/lib/manager/SubscriptionManager.js +1 -1
  47. package/lib/next/index.js +1 -1
  48. package/lib/state/reducer/createReducer.d.ts.map +1 -1
  49. package/lib/state/reducer/createReducer.js +6 -3
  50. package/lib/state/reducer/expireReducer.d.ts +1 -1
  51. package/lib/state/reducer/fetchReducer.js +1 -1
  52. package/lib/state/reducer/invalidateReducer.d.ts +1 -1
  53. package/lib/state/reducer/setReducer.d.ts +2 -3
  54. package/lib/state/reducer/setReducer.d.ts.map +1 -1
  55. package/lib/state/reducer/setReducer.js +7 -90
  56. package/lib/state/reducer/setResponseReducer.d.ts +4 -0
  57. package/lib/state/reducer/setResponseReducer.d.ts.map +1 -0
  58. package/lib/state/reducer/setResponseReducer.js +112 -0
  59. package/lib/types.d.ts +2 -4
  60. package/lib/types.d.ts.map +1 -1
  61. package/lib/types.js +1 -1
  62. package/package.json +1 -1
  63. package/src/actionTypes.ts +1 -0
  64. package/src/actions.ts +29 -10
  65. package/src/controller/Controller.ts +25 -8
  66. package/src/controller/createSet.ts +11 -51
  67. package/src/controller/createSetResponse.ts +79 -0
  68. package/src/index.ts +1 -0
  69. package/src/manager/DevtoolsManager.ts +4 -2
  70. package/src/manager/LogoutManager.ts +2 -2
  71. package/src/manager/NetworkManager.ts +6 -6
  72. package/src/manager/__tests__/logoutManager.ts +5 -5
  73. package/src/manager/__tests__/networkManager.ts +6 -6
  74. package/src/manager/__tests__/subscriptionManager.ts +2 -2
  75. package/src/state/__tests__/reducer.ts +64 -25
  76. package/src/state/reducer/createReducer.ts +6 -1
  77. package/src/state/reducer/fetchReducer.ts +2 -2
  78. package/src/state/reducer/setReducer.ts +9 -121
  79. package/src/state/reducer/setResponseReducer.ts +151 -0
  80. package/src/types.ts +6 -5
  81. package/ts3.4/actionTypes.d.ts +1 -0
  82. package/ts3.4/actions.d.ts +23 -11
  83. package/ts3.4/controller/Controller.d.ts +10 -2
  84. package/ts3.4/controller/createSet.d.ts +5 -19
  85. package/ts3.4/controller/createSetResponse.d.ts +24 -0
  86. package/ts3.4/index.d.ts +1 -0
  87. package/ts3.4/manager/NetworkManager.d.ts +2 -2
  88. package/ts3.4/state/reducer/expireReducer.d.ts +1 -1
  89. package/ts3.4/state/reducer/invalidateReducer.d.ts +1 -1
  90. package/ts3.4/state/reducer/setReducer.d.ts +2 -3
  91. package/ts3.4/state/reducer/setResponseReducer.d.ts +4 -0
  92. package/ts3.4/types.d.ts +2 -4
@@ -2,7 +2,7 @@ import { CoolerArticleResource } from '__tests__/new';
2
2
 
3
3
  import { Controller, initialState } from '../..';
4
4
  import { FETCH_TYPE, RESET_TYPE } from '../../actionTypes';
5
- import createSet from '../../controller/createSet';
5
+ import createSetResponse from '../../controller/createSetResponse';
6
6
  import LogoutManager from '../LogoutManager.js';
7
7
 
8
8
  function onError(e: any) {
@@ -44,7 +44,7 @@ describe('LogoutManager', () => {
44
44
  },
45
45
  );
46
46
  it('should ignore non-error set', async () => {
47
- const action = createSet(CoolerArticleResource.get, {
47
+ const action = createSetResponse(CoolerArticleResource.get, {
48
48
  args: [{ id: 5 }],
49
49
  response: { id: 5, title: 'hi' },
50
50
  });
@@ -55,7 +55,7 @@ describe('LogoutManager', () => {
55
55
  it('should ignore non-401 set', async () => {
56
56
  const error: any = new Error('network failed');
57
57
  error.status = 404;
58
- const action = createSet(CoolerArticleResource.get, {
58
+ const action = createSetResponse(CoolerArticleResource.get, {
59
59
  args: [{ id: 5 }],
60
60
  response: error,
61
61
  error: true,
@@ -67,7 +67,7 @@ describe('LogoutManager', () => {
67
67
  it('should dispatch reset on 401', async () => {
68
68
  const error: any = new Error('network failed');
69
69
  error.status = 401;
70
- const action = createSet(CoolerArticleResource.get, {
70
+ const action = createSetResponse(CoolerArticleResource.get, {
71
71
  args: [{ id: 5 }],
72
72
  response: error,
73
73
  error: true,
@@ -88,7 +88,7 @@ describe('LogoutManager', () => {
88
88
  }).getMiddleware();
89
89
  const error: any = new Error('network failed');
90
90
  error.status = 403;
91
- const action = createSet(CoolerArticleResource.get, {
91
+ const action = createSetResponse(CoolerArticleResource.get, {
92
92
  args: [{ id: 5 }],
93
93
  response: error,
94
94
  error: true,
@@ -1,7 +1,7 @@
1
1
  import { Endpoint } from '@data-client/endpoint';
2
2
  import { Article, ArticleResource } from '__tests__/new';
3
3
 
4
- import { SET_TYPE } from '../../actionTypes';
4
+ import { SET_RESPONSE_TYPE } from '../../actionTypes';
5
5
  import Controller from '../../controller/Controller';
6
6
  import createFetch from '../../controller/createFetch';
7
7
  import NetworkManager from '../../manager/NetworkManager';
@@ -160,7 +160,7 @@ describe('NetworkManager', () => {
160
160
  await new Promise(resolve => setTimeout(resolve, 0));
161
161
 
162
162
  const action = {
163
- type: SET_TYPE,
163
+ type: SET_RESPONSE_TYPE,
164
164
  endpoint: fetchResolveAction.endpoint,
165
165
  payload: data,
166
166
  meta: {
@@ -193,7 +193,7 @@ describe('NetworkManager', () => {
193
193
  await new Promise(resolve => setTimeout(resolve, 0));
194
194
 
195
195
  const action = {
196
- type: SET_TYPE,
196
+ type: SET_RESPONSE_TYPE,
197
197
  endpoint: fetchSetWithUpdatersAction.endpoint,
198
198
  payload: data,
199
199
  meta: {
@@ -226,7 +226,7 @@ describe('NetworkManager', () => {
226
226
  await new Promise(resolve => setTimeout(resolve, 0));
227
227
 
228
228
  const action = {
229
- type: SET_TYPE,
229
+ type: SET_RESPONSE_TYPE,
230
230
  endpoint: fetchRpcWithUpdatersAction.endpoint,
231
231
  payload: data,
232
232
  meta: {
@@ -259,7 +259,7 @@ describe('NetworkManager', () => {
259
259
  // mutations resolve before dispatch, so we must wait for next tick to see set
260
260
  await new Promise(resolve => setTimeout(resolve, 0));
261
261
  expect(dispatch).toHaveBeenCalledWith({
262
- type: SET_TYPE,
262
+ type: SET_RESPONSE_TYPE,
263
263
  endpoint: fetchRpcWithUpdatersAndOptimisticAction.endpoint,
264
264
  payload: data,
265
265
  meta: {
@@ -329,7 +329,7 @@ describe('NetworkManager', () => {
329
329
  } catch (error) {
330
330
  expect(next).not.toHaveBeenCalled();
331
331
  expect(dispatch).toHaveBeenCalledWith({
332
- type: SET_TYPE,
332
+ type: SET_RESPONSE_TYPE,
333
333
  payload: error,
334
334
  meta: {
335
335
  key: fetchRejectAction.meta.key,
@@ -4,7 +4,7 @@ import { actionTypes, Controller, initialState } from '../..';
4
4
  import { SubscribeAction, UnsubscribeAction } from '../../types';
5
5
  import SubscriptionManager, { Subscription } from '../SubscriptionManager.js';
6
6
 
7
- const { UNSUBSCRIBE_TYPE, SUBSCRIBE_TYPE, SET_TYPE } = actionTypes;
7
+ const { UNSUBSCRIBE_TYPE, SUBSCRIBE_TYPE, SET_RESPONSE_TYPE } = actionTypes;
8
8
 
9
9
  function onError(e: any) {
10
10
  e.preventDefault();
@@ -192,7 +192,7 @@ describe('SubscriptionManager', () => {
192
192
  });
193
193
 
194
194
  it('should let other actions pass through', () => {
195
- const action = { type: SET_TYPE };
195
+ const action = { type: SET_RESPONSE_TYPE };
196
196
  next.mockReset();
197
197
 
198
198
  middleware(API)(next)(action as any);
@@ -1,29 +1,24 @@
1
1
  import { INVALID } from '@data-client/endpoint';
2
- import {
3
- ArticleResource,
4
- PaginatedArticleResource,
5
- Article,
6
- PaginatedArticle,
7
- } from '__tests__/new';
2
+ import { ArticleResource, Article, PaginatedArticle } from '__tests__/new';
8
3
 
9
4
  import { Controller } from '../..';
10
5
  import {
11
- SET_TYPE,
12
6
  INVALIDATE_TYPE,
13
7
  FETCH_TYPE,
14
8
  RESET_TYPE,
15
9
  GC_TYPE,
10
+ SET_RESPONSE_TYPE,
11
+ SET_TYPE,
16
12
  } from '../../actionTypes';
17
- import createSet from '../../controller/createSet';
18
13
  import {
19
- UpdateFunction,
20
14
  State,
21
15
  ActionTypes,
22
16
  FetchAction,
23
- SetAction,
17
+ SetResponseAction,
24
18
  ResetAction,
25
19
  InvalidateAction,
26
20
  GCAction,
21
+ SetAction,
27
22
  } from '../../types';
28
23
  import createReducer, { initialState } from '../reducer/createReducer';
29
24
 
@@ -40,8 +35,8 @@ describe('reducer', () => {
40
35
  describe('singles', () => {
41
36
  const id = 20;
42
37
  const payload = { id, title: 'hi', content: 'this is the content' };
43
- const action: SetAction = {
44
- type: SET_TYPE,
38
+ const action: SetResponseAction = {
39
+ type: SET_RESPONSE_TYPE,
45
40
  payload,
46
41
  endpoint: ArticleResource.get,
47
42
  meta: {
@@ -207,11 +202,55 @@ describe('reducer', () => {
207
202
  });
208
203
  });
209
204
 
210
- it('mutate should never change endpoints', () => {
205
+ it('set should add entity when it does not exist', () => {
211
206
  const id = 20;
212
- const payload = { id, title: 'hi', content: 'this is the content' };
207
+ const value = { id, title: 'hi', content: 'this is the content' };
208
+ const action: SetAction = {
209
+ type: SET_TYPE,
210
+ value,
211
+ schema: Article,
212
+ meta: {
213
+ args: [{ id }],
214
+ date: 0,
215
+ fetchedAt: 0,
216
+ expiresAt: 1000000000000,
217
+ },
218
+ };
219
+ const iniState = {
220
+ ...initialState,
221
+ endpoints: { abc: '5', [ArticleResource.get.key(value)]: `${id}` },
222
+ };
223
+ const newState = reducer(iniState, action);
224
+ expect(newState.entities[Article.key]?.[id]).toEqual(value);
225
+ });
226
+
227
+ it('set should never change endpoints', () => {
228
+ const id = 20;
229
+ const value = { id, title: 'hi', content: 'this is the content' };
213
230
  const action: SetAction = {
214
231
  type: SET_TYPE,
232
+ value,
233
+ schema: Article,
234
+ meta: {
235
+ args: [{ id }],
236
+ date: 0,
237
+ fetchedAt: 0,
238
+ expiresAt: 1000000000000,
239
+ },
240
+ };
241
+ const iniState = {
242
+ ...initialState,
243
+ endpoints: { abc: '5', [ArticleResource.get.key(value)]: `${id}` },
244
+ };
245
+ const newState = reducer(iniState, action);
246
+ expect(newState.endpoints).toStrictEqual(iniState.endpoints);
247
+ });
248
+
249
+ it('mutate should never change endpoints', () => {
250
+ const id = 20;
251
+ const payload = { id, title: 'hi', content: 'this is the content' };
252
+ const action: SetResponseAction = {
253
+ type: SET_RESPONSE_TYPE,
215
254
  payload,
216
255
  endpoint: ArticleResource.get,
217
256
  meta: {
@@ -231,8 +270,8 @@ describe('reducer', () => {
231
270
  });
232
271
  it('purge should delete entities', () => {
233
272
  const id = 20;
234
- const action: SetAction = {
235
- type: SET_TYPE,
273
+ const action: SetResponseAction = {
274
+ type: SET_RESPONSE_TYPE,
236
275
  payload: { id },
237
276
  endpoint: ArticleResource.delete,
238
277
  meta: {
@@ -283,7 +322,7 @@ describe('reducer', () => {
283
322
  };
284
323
 
285
324
  it('should insert a new page of resources into a list request', () => {
286
- const action = createSet(
325
+ const action = createSetResponse(
287
326
  { results: [{ id: 11 }, { id: 12 }] },
288
327
  {
289
328
  ...endpoint,
@@ -310,7 +349,7 @@ describe('reducer', () => {
310
349
  it('should insert correctly into the beginning of the list request', () => {
311
350
  const newState = reducer(
312
351
  iniState,
313
- createSet(
352
+ createSetResponse(
314
353
  { results: [{ id: 11 }, { id: 12 }] },
315
354
  {
316
355
  ...endpoint,
@@ -350,7 +389,7 @@ describe('reducer', () => {
350
389
  };
351
390
  const newState = reducer(
352
391
  iniState,
353
- createSet(
392
+ createSetResponse(
354
393
  { results: [{ id: 11 }, { id: 12 }] },
355
394
  {
356
395
  ...endpoint,
@@ -420,8 +459,8 @@ describe('reducer', () => {
420
459
  it('should set error in meta for "set"', () => {
421
460
  const id = 20;
422
461
  const error = new Error('hi');
423
- const action: SetAction = {
424
- type: SET_TYPE,
462
+ const action: SetResponseAction = {
463
+ type: SET_RESPONSE_TYPE,
425
464
  payload: error,
426
465
  endpoint: ArticleResource.get,
427
466
  meta: {
@@ -440,8 +479,8 @@ describe('reducer', () => {
440
479
  it('should not modify state on error for "rpc"', () => {
441
480
  const id = 20;
442
481
  const error = new Error('hi');
443
- const action: SetAction = {
444
- type: SET_TYPE,
482
+ const action: SetResponseAction = {
483
+ type: SET_RESPONSE_TYPE,
445
484
  payload: error,
446
485
  endpoint: ArticleResource.get,
447
486
  meta: {
@@ -461,8 +500,8 @@ describe('reducer', () => {
461
500
  it('should not delete on error for "purge"', () => {
462
501
  const id = 20;
463
502
  const error = new Error('hi');
464
- const action: SetAction = {
465
- type: SET_TYPE,
503
+ const action: SetResponseAction = {
504
+ type: SET_RESPONSE_TYPE,
466
505
  payload: error,
467
506
  endpoint: ArticleResource.delete,
468
507
  meta: {
@@ -2,6 +2,7 @@ import { expireReducer } from './expireReducer.js';
2
2
  import { fetchReducer } from './fetchReducer.js';
3
3
  import { invalidateReducer } from './invalidateReducer.js';
4
4
  import { setReducer } from './setReducer.js';
5
+ import { setResponseReducer } from './setResponseReducer.js';
5
6
  import {
6
7
  SET_TYPE,
7
8
  INVALIDATE_TYPE,
@@ -11,6 +12,7 @@ import {
11
12
  OPTIMISTIC_TYPE,
12
13
  INVALIDATEALL_TYPE,
13
14
  EXPIREALL_TYPE,
15
+ SET_RESPONSE_TYPE,
14
16
  } from '../../actionTypes.js';
15
17
  import type Controller from '../../controller/Controller.js';
16
18
  import type { ActionTypes, State } from '../../types.js';
@@ -38,8 +40,11 @@ export default function createReducer(controller: Controller): ReducerType {
38
40
 
39
41
  case OPTIMISTIC_TYPE:
40
42
  // eslint-disable-next-line no-fallthrough
43
+ case SET_RESPONSE_TYPE:
44
+ return setResponseReducer(state, action, controller);
45
+
41
46
  case SET_TYPE:
42
- return setReducer(state, action, controller);
47
+ return setReducer(state, action);
43
48
 
44
49
  case INVALIDATEALL_TYPE:
45
50
  case INVALIDATE_TYPE:
@@ -1,13 +1,13 @@
1
1
  import createOptimistic from '../../controller/createOptimistic.js';
2
2
  import type {
3
3
  State,
4
- SetAction,
4
+ SetResponseAction,
5
5
  OptimisticAction,
6
6
  FetchAction,
7
7
  } from '../../types.js';
8
8
 
9
9
  export function fetchReducer(state: State<unknown>, action: FetchAction) {
10
- let setAction: SetAction | OptimisticAction;
10
+ let setAction: SetResponseAction | OptimisticAction;
11
11
 
12
12
  if (action.endpoint.getOptimisticResponse && action.endpoint.sideEffect) {
13
13
  setAction = createOptimistic(action.endpoint, {
@@ -1,146 +1,34 @@
1
1
  import { normalize } from '@data-client/normalizr';
2
2
 
3
- import { OPTIMISTIC_TYPE } from '../../actionTypes.js';
4
- import AbortOptimistic from '../../controller/AbortOptimistic.js';
5
- import type Controller from '../../controller/Controller.js';
6
- import type { State, SetAction, OptimisticAction } from '../../types.js';
3
+ import type { State, SetAction } from '../../types.js';
7
4
 
8
- export function setReducer(
9
- state: State<unknown>,
10
- action: OptimisticAction | SetAction,
11
- controller: Controller,
12
- ) {
13
- if (action.error) {
14
- return reduceError(state, action, action.payload);
15
- }
5
+ export function setReducer(state: State<unknown>, action: SetAction) {
16
6
  try {
17
- let payload: any;
18
- // for true set's payload is contained in action
19
- if (action.type === OPTIMISTIC_TYPE) {
20
- // this should never happen
21
- if (!action.endpoint.getOptimisticResponse) return state;
22
- try {
23
- // compute optimistic response based on current state
24
- payload = action.endpoint.getOptimisticResponse.call(
25
- action.endpoint,
26
- controller.snapshot(state, action.meta.fetchedAt),
27
- ...action.meta.args,
28
- );
29
- } catch (e: any) {
30
- // AbortOptimistic means 'do nothing', otherwise we count the exception as endpoint failure
31
- if (e.constructor === AbortOptimistic) {
32
- return state;
33
- }
34
- throw e;
35
- }
36
- } else {
37
- payload = action.payload;
38
- }
39
- const { result, entities, indexes, entityMeta } = normalize(
40
- payload,
41
- action.endpoint.schema,
7
+ const { entities, indexes, entityMeta } = normalize(
8
+ action.value,
9
+ action.schema,
42
10
  action.meta.args as any,
43
11
  state.entities,
44
12
  state.indexes,
45
13
  state.entityMeta,
46
14
  action.meta,
47
15
  );
48
- const endpoints = {
49
- ...state.endpoints,
50
- [action.meta.key]: result,
51
- };
52
- try {
53
- if (action.endpoint.update) {
54
- const updaters = action.endpoint.update(result, ...action.meta.args);
55
- Object.keys(updaters).forEach(key => {
56
- endpoints[key] = updaters[key](endpoints[key]);
57
- });
58
- }
59
- // no reason to completely fail because of user-code error
60
- // integrity of this state update is still guaranteed
61
- } catch (error) {
62
- console.error(
63
- `The following error occured during Endpoint.update() for ${action.meta.key}`,
64
- );
65
- console.error(error);
66
- }
67
16
  return {
68
17
  entities,
69
18
  indexes,
70
- endpoints,
19
+ endpoints: state.endpoints,
71
20
  entityMeta,
72
- meta: {
73
- ...state.meta,
74
- [action.meta.key]: {
75
- date: action.meta.date,
76
- expiresAt: action.meta.expiresAt,
77
- prevExpiresAt: state.meta[action.meta.key]?.expiresAt,
78
- },
79
- },
80
- optimistic: filterOptimistic(state, action),
21
+ meta: state.meta,
22
+ optimistic: state.optimistic,
81
23
  lastReset: state.lastReset,
82
24
  };
83
25
  // reducer must update the state, so in case of processing errors we simply compute the endpoints inline
84
26
  } catch (error: any) {
85
- if (typeof error === 'object') {
86
- error.message = `Error processing ${
87
- action.meta.key
88
- }\n\nFull Schema: ${JSON.stringify(
89
- action.endpoint.schema,
90
- undefined,
91
- 2,
92
- )}\n\nError:\n${error.message}`;
93
- if ('payload' in action) error.payload = action.payload;
94
- error.status = 400;
95
- }
96
-
97
27
  // this is not always bubbled up, so let's double sure this doesn't fail silently
98
28
  /* istanbul ignore else */
99
29
  if (process.env.NODE_ENV !== 'production') {
100
30
  console.error(error);
101
31
  }
102
- return reduceError(state, action, error);
32
+ return state;
103
33
  }
104
34
  }
105
-
106
- function reduceError(
107
- state: State<unknown>,
108
- action: SetAction | OptimisticAction,
109
- error: any,
110
- ): State<unknown> {
111
- if (error.name === 'AbortError') {
112
- // In case we abort simply undo the optimistic update and act like no fetch even occured
113
- // We still want those watching promises from fetch directly to observed the abort, but we don't want to
114
- // Trigger errors in this case. This means theoretically improperly built abortes useResource() could suspend forever.
115
- return {
116
- ...state,
117
- optimistic: filterOptimistic(state, action),
118
- };
119
- }
120
- return {
121
- ...state,
122
- meta: {
123
- ...state.meta,
124
- [action.meta.key]: {
125
- date: action.meta.date,
126
- error,
127
- expiresAt: action.meta.expiresAt,
128
- errorPolicy: action.endpoint.errorPolicy?.(error),
129
- },
130
- },
131
- optimistic: filterOptimistic(state, action),
132
- };
133
- }
134
- /** Filter all requests with same serialization that did not start after the resolving request */
135
- function filterOptimistic(
136
- state: State<unknown>,
137
- resolvingAction: SetAction | OptimisticAction,
138
- ) {
139
- return state.optimistic.filter(
140
- optimisticAction =>
141
- optimisticAction.meta.key !== resolvingAction.meta.key ||
142
- (optimisticAction.type === OPTIMISTIC_TYPE ?
143
- optimisticAction.meta.fetchedAt !== resolvingAction.meta.fetchedAt
144
- : optimisticAction.meta.date > resolvingAction.meta.date),
145
- );
146
- }
@@ -0,0 +1,151 @@
1
+ import { normalize } from '@data-client/normalizr';
2
+
3
+ import { OPTIMISTIC_TYPE } from '../../actionTypes.js';
4
+ import AbortOptimistic from '../../controller/AbortOptimistic.js';
5
+ import type Controller from '../../controller/Controller.js';
6
+ import type {
7
+ State,
8
+ SetResponseAction,
9
+ OptimisticAction,
10
+ } from '../../types.js';
11
+
12
+ export function setResponseReducer(
13
+ state: State<unknown>,
14
+ action: OptimisticAction | SetResponseAction,
15
+ controller: Controller,
16
+ ) {
17
+ if (action.error) {
18
+ return reduceError(state, action, action.payload);
19
+ }
20
+ try {
21
+ let payload: any;
22
+ // for true set's payload is contained in action
23
+ if (action.type === OPTIMISTIC_TYPE) {
24
+ // this should never happen
25
+ /* istanbul ignore if */
26
+ if (!action.endpoint.getOptimisticResponse) return state;
27
+ try {
28
+ // compute optimistic response based on current state
29
+ payload = action.endpoint.getOptimisticResponse.call(
30
+ action.endpoint,
31
+ controller.snapshot(state, action.meta.fetchedAt),
32
+ ...action.meta.args,
33
+ );
34
+ } catch (e: any) {
35
+ // AbortOptimistic means 'do nothing', otherwise we count the exception as endpoint failure
36
+ if (e.constructor === AbortOptimistic) {
37
+ return state;
38
+ }
39
+ throw e;
40
+ }
41
+ } else {
42
+ payload = action.payload;
43
+ }
44
+ const { result, entities, indexes, entityMeta } = normalize(
45
+ payload,
46
+ action.endpoint.schema,
47
+ action.meta.args as any,
48
+ state.entities,
49
+ state.indexes,
50
+ state.entityMeta,
51
+ action.meta,
52
+ );
53
+ const endpoints: Record<string, unknown> = {
54
+ ...state.endpoints,
55
+ [action.meta.key]: result,
56
+ };
57
+ try {
58
+ if (action.endpoint.update) {
59
+ const updaters = action.endpoint.update(result, ...action.meta.args);
60
+ Object.keys(updaters).forEach(key => {
61
+ endpoints[key] = updaters[key](endpoints[key]);
62
+ });
63
+ }
64
+ // no reason to completely fail because of user-code error
65
+ // integrity of this state update is still guaranteed
66
+ } catch (error) {
67
+ console.error(
68
+ `The following error occured during Endpoint.update() for ${action.meta.key}`,
69
+ );
70
+ console.error(error);
71
+ }
72
+ return {
73
+ entities,
74
+ indexes,
75
+ endpoints,
76
+ entityMeta,
77
+ meta: {
78
+ ...state.meta,
79
+ [action.meta.key]: {
80
+ date: action.meta.date,
81
+ expiresAt: action.meta.expiresAt,
82
+ prevExpiresAt: state.meta[action.meta.key]?.expiresAt,
83
+ },
84
+ },
85
+ optimistic: filterOptimistic(state, action),
86
+ lastReset: state.lastReset,
87
+ };
88
+ // reducer must update the state, so in case of processing errors we simply compute the endpoints inline
89
+ } catch (error: any) {
90
+ if (typeof error === 'object') {
91
+ error.message = `Error processing ${
92
+ action.meta.key
93
+ }\n\nFull Schema: ${JSON.stringify(
94
+ action.endpoint.schema,
95
+ undefined,
96
+ 2,
97
+ )}\n\nError:\n${error.message}`;
98
+ if ('payload' in action) error.payload = action.payload;
99
+ error.status = 400;
100
+ }
101
+
102
+ // this is not always bubbled up, so let's double sure this doesn't fail silently
103
+ /* istanbul ignore else */
104
+ if (process.env.NODE_ENV !== 'production') {
105
+ console.error(error);
106
+ }
107
+ return reduceError(state, action, error);
108
+ }
109
+ }
110
+
111
+ function reduceError(
112
+ state: State<unknown>,
113
+ action: SetResponseAction | OptimisticAction,
114
+ error: any,
115
+ ): State<unknown> {
116
+ if (error.name === 'AbortError') {
117
+ // In case we abort simply undo the optimistic update and act like no fetch even occured
118
+ // We still want those watching promises from fetch directly to observed the abort, but we don't want to
119
+ // Trigger errors in this case. This means theoretically improperly built abortes useResource() could suspend forever.
120
+ return {
121
+ ...state,
122
+ optimistic: filterOptimistic(state, action),
123
+ };
124
+ }
125
+ return {
126
+ ...state,
127
+ meta: {
128
+ ...state.meta,
129
+ [action.meta.key]: {
130
+ date: action.meta.date,
131
+ error,
132
+ expiresAt: action.meta.expiresAt,
133
+ errorPolicy: action.endpoint.errorPolicy?.(error),
134
+ },
135
+ },
136
+ optimistic: filterOptimistic(state, action),
137
+ };
138
+ }
139
+ /** Filter all requests with same serialization that did not start after the resolving request */
140
+ function filterOptimistic(
141
+ state: State<unknown>,
142
+ resolvingAction: SetResponseAction | OptimisticAction,
143
+ ) {
144
+ return state.optimistic.filter(
145
+ optimisticAction =>
146
+ optimisticAction.meta.key !== resolvingAction.meta.key ||
147
+ (optimisticAction.type === OPTIMISTIC_TYPE ?
148
+ optimisticAction.meta.fetchedAt !== resolvingAction.meta.fetchedAt
149
+ : optimisticAction.meta.date > resolvingAction.meta.date),
150
+ );
151
+ }
package/src/types.ts CHANGED
@@ -5,14 +5,15 @@ import type {
5
5
  } from '@data-client/normalizr';
6
6
  import type { ErrorTypes } from '@data-client/normalizr';
7
7
 
8
- import type { ActionTypes, SetAction, OptimisticAction } from './actions.js';
9
- import { SET_TYPE } from './actionTypes.js';
8
+ import type {
9
+ ActionTypes,
10
+ SetResponseAction,
11
+ OptimisticAction,
12
+ } from './actions.js';
10
13
  import type { Dispatch, Middleware, MiddlewareAPI } from './middlewareTypes.js';
11
14
 
12
15
  export type { AbstractInstanceType, UpdateFunction };
13
16
 
14
- export type SetTypes = typeof SET_TYPE;
15
-
16
17
  export type PK = string;
17
18
 
18
19
  export interface State<T> {
@@ -42,7 +43,7 @@ export interface State<T> {
42
43
  };
43
44
  };
44
45
  };
45
- readonly optimistic: (SetAction | OptimisticAction)[];
46
+ readonly optimistic: (SetResponseAction | OptimisticAction)[];
46
47
  readonly lastReset: number;
47
48
  }
48
49
 
@@ -1,5 +1,6 @@
1
1
  export declare const FETCH_TYPE: "rdc/fetch";
2
2
  export declare const SET_TYPE: "rdc/set";
3
+ export declare const SET_RESPONSE_TYPE: "rdc/setresponse";
3
4
  export declare const OPTIMISTIC_TYPE: "rdc/optimistic";
4
5
  export declare const RESET_TYPE: "rdc/reset";
5
6
  export declare const SUBSCRIBE_TYPE: "rdc/subscribe";