@civet/core 1.2.6 → 1.3.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.
@@ -1,8 +1,6 @@
1
1
  import PropTypes from 'prop-types';
2
- import React from 'react';
3
-
4
- import { ResourceContext } from './context';
5
2
  import { dataProviderPropType } from './DataProvider';
3
+ import { ResourceContext } from './context';
6
4
  import useResource from './useResource';
7
5
 
8
6
  const propTypes = {
@@ -27,12 +25,12 @@ const propTypes = {
27
25
  * The provided DataProvider must not be replaced.
28
26
  */
29
27
  function Resource({ dataProvider, name, query, empty, options, persistent, children, ...rest }) {
30
- const context = useResource({ dataProvider, name, query, empty, options, persistent });
28
+ const context = useResource({ dataProvider, name, query, empty, options, persistent, ...rest });
31
29
 
32
30
  return context.dataProvider.uiPlugins.reduceRight(
33
- (next, Plugin) => (result) =>
34
- (
35
- // eslint-disable-next-line react/jsx-props-no-spreading
31
+ (next, Plugin) =>
32
+ // eslint-disable-next-line react/display-name
33
+ (result) => (
36
34
  <Plugin {...rest} context={result}>
37
35
  {next}
38
36
  </Plugin>
package/src/context.js CHANGED
@@ -1,11 +1,11 @@
1
- import React from 'react';
1
+ import { createContext, useContext } from 'react';
2
2
 
3
3
  const noop = () => {};
4
4
 
5
- export const ConfigContext = React.createContext({});
5
+ export const ConfigContext = createContext({});
6
6
  ConfigContext.displayName = 'ConfigContext';
7
- export const useConfigContext = () => React.useContext(ConfigContext);
7
+ export const useConfigContext = () => useContext(ConfigContext);
8
8
 
9
- export const ResourceContext = React.createContext({ data: [], notify: noop });
9
+ export const ResourceContext = createContext({ data: [], notify: noop });
10
10
  ResourceContext.displayName = 'ResourceContext';
11
- export const useResourceContext = () => React.useContext(ResourceContext);
11
+ export const useResourceContext = () => useContext(ResourceContext);
@@ -4,7 +4,6 @@ function createPlugin(plugin) {
4
4
  if (typeof plugin !== 'function') throw new Error('No valid plugin definition specified');
5
5
  return (dataProviderClass) => {
6
6
  if (!Object.prototype.isPrototypeOf.call(DataProvider, dataProviderClass)) {
7
- // eslint-disable-next-line no-console
8
7
  console.error(
9
8
  'A plugin should be given a derivative of the DataProvider class as its first parameter',
10
9
  );
package/src/index.js CHANGED
@@ -4,12 +4,12 @@ export const { Consumer: ConfigConsumer } = ConfigContext;
4
4
  export const { Provider: ResourceProvider, Consumer: ResourceConsumer } = ResourceContext;
5
5
  export { default as AbortSignal } from './AbortSignal';
6
6
  export { default as ChannelNotifier } from './ChannelNotifier';
7
- export { default as compose } from './compose';
8
7
  export { default as ConfigProvider } from './ConfigProvider';
9
- export { default as createPlugin } from './createPlugin';
10
- export { dataProviderPropType, default as DataProvider, isDataProvider } from './DataProvider';
8
+ export { default as DataProvider, dataProviderPropType, isDataProvider } from './DataProvider';
11
9
  export { default as Meta } from './Meta';
12
10
  export { default as Notifier } from './Notifier';
13
11
  export { default as Resource } from './Resource';
12
+ export { default as compose } from './compose';
13
+ export { default as createPlugin } from './createPlugin';
14
14
  export { default as useResource } from './useResource';
15
15
  export { useConfigContext, useResourceContext };
@@ -1,12 +1,166 @@
1
- import React from 'react';
2
-
1
+ import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
3
2
  import AbortSignal from './AbortSignal';
4
3
  import Meta from './Meta';
5
4
  import { useConfigContext } from './context';
6
5
  import uniqueIdentifier from './uniqueIdentifier';
7
6
 
8
7
  /**
9
- * Makes data from an DataProvider available.
8
+ * Appends a new requestInstruction to the state, causing the resource to fetch data.
9
+ **/
10
+ function createRequestInstruction(state) {
11
+ return {
12
+ ...state,
13
+ requestInstruction: {
14
+ dataProvider: state.dataProvider,
15
+ requestDetails: state.requestDetails,
16
+ request: state.request,
17
+ revision: state.revision,
18
+ value: state.value,
19
+ },
20
+ };
21
+ }
22
+
23
+ /**
24
+ * State reducer for the resource.
25
+ */
26
+ function reducer(state, action) {
27
+ switch (action.type) {
28
+ // Creates a new request and instructs the resource to fetch data.
29
+ case 'next-request': {
30
+ const { requestDetails: nextRequestDetails, persistent: nextPersistent } = action;
31
+ const nextRequest = uniqueIdentifier(state.request);
32
+ const nextRevision = uniqueIdentifier(state.revision);
33
+ let isPersistent = false;
34
+ if (state.persistent === 'very' && nextPersistent === 'very') {
35
+ isPersistent = 'very';
36
+ } else if (state.persistent && nextPersistent) {
37
+ isPersistent = true;
38
+ }
39
+ const shouldValuePersist =
40
+ !nextRequestDetails.empty &&
41
+ state.dataProvider.shouldPersist(
42
+ nextRequestDetails,
43
+ state.requestDetails,
44
+ isPersistent,
45
+ state.value,
46
+ );
47
+ return createRequestInstruction({
48
+ dataProvider: state.dataProvider,
49
+ requestDetails: nextRequestDetails,
50
+ request: nextRequest,
51
+ revision: nextRevision,
52
+ isLoading: !nextRequestDetails.empty,
53
+ value: shouldValuePersist
54
+ ? state.value
55
+ : {
56
+ name: nextRequestDetails.name,
57
+ query: nextRequestDetails.query,
58
+ options: nextRequestDetails.options,
59
+ request: nextRequest,
60
+ revision: nextRevision,
61
+ data: [],
62
+ meta: {},
63
+ error: undefined,
64
+ isEmpty: !!nextRequestDetails.empty,
65
+ isIncomplete: !nextRequestDetails.empty,
66
+ isInitial: !nextRequestDetails.empty,
67
+ },
68
+ persistent: nextPersistent,
69
+ });
70
+ }
71
+
72
+ // Creates a new revision for the current request and instructs the resource to fetch data.
73
+ case 'next-revision': {
74
+ const { notify } = action;
75
+ const nextRevision = uniqueIdentifier(state.revision);
76
+ notify({ request: state.request, revison: nextRevision });
77
+ return createRequestInstruction({
78
+ ...state,
79
+ revision: nextRevision,
80
+ isLoading: !state.requestDetails.empty,
81
+ });
82
+ }
83
+
84
+ // Sets a new persistence level.
85
+ case 'set-persistence': {
86
+ const { persistent: nextPersistent } = action;
87
+ return {
88
+ ...state,
89
+ persistent: nextPersistent,
90
+ };
91
+ }
92
+
93
+ // Updates the current request's data.
94
+ case 'update-data': {
95
+ const { request, revision, value } = action;
96
+ if (request !== state.request || revision !== state.revision) return state;
97
+ return {
98
+ ...state,
99
+ isLoading: value.isIncomplete,
100
+ value,
101
+ };
102
+ }
103
+ }
104
+
105
+ return state;
106
+ }
107
+
108
+ /**
109
+ * Starts fetching data and updates the resource when new data is available.
110
+ */
111
+ function fetchData(requestInstruction, instance, abortSignal, dispatch) {
112
+ const { dataProvider, requestDetails, request, revision, value } = requestInstruction;
113
+
114
+ const meta = new Meta({ ...value.meta }, instance);
115
+
116
+ let promise = Promise.resolve(value);
117
+
118
+ const callback = (error, done, data) => {
119
+ promise = promise.then((prevValue) => {
120
+ try {
121
+ let nextValue;
122
+ if (error != null) {
123
+ nextValue = { ...prevValue, error, isIncomplete: false };
124
+ } else {
125
+ const context = {
126
+ name: requestDetails.name,
127
+ query: requestDetails.query,
128
+ options: requestDetails.options,
129
+ request,
130
+ revision,
131
+ data,
132
+ meta: meta.commit(prevValue.meta),
133
+ error: undefined,
134
+ isEmpty: false,
135
+ isIncomplete: !done,
136
+ isInitial: !!prevValue.isInitial && !done,
137
+ };
138
+ context.data = dataProvider.transition(context, prevValue);
139
+ context.data = dataProvider.recycleItems(context, prevValue);
140
+ nextValue = context;
141
+ }
142
+
143
+ dispatch({ type: 'update-data', request, revision, value: nextValue });
144
+
145
+ return nextValue;
146
+ } catch {
147
+ return prevValue;
148
+ }
149
+ });
150
+ };
151
+
152
+ dataProvider.continuousGet(
153
+ requestDetails.name,
154
+ requestDetails.query,
155
+ requestDetails.options,
156
+ meta,
157
+ callback,
158
+ abortSignal,
159
+ );
160
+ }
161
+
162
+ /**
163
+ * Makes data from a DataProvider available.
10
164
  * If not explicitly specified, necessary configuration is taken from the nearest <ConfigProvider>.
11
165
  * The provided DataProvider must not be replaced.
12
166
  */
@@ -21,35 +175,16 @@ function useResource({
21
175
  }) {
22
176
  const configContext = useConfigContext();
23
177
  const currentDataProvider = dataProviderProp || configContext.dataProvider;
24
- const [dataProvider] = React.useState(currentDataProvider);
25
- if (dataProvider == null) {
26
- throw new Error(
27
- 'Unmet requirement: The DataProvider for the useResource hook is missing - Check your ConfigContext provider and the dataProvider property',
28
- );
29
- }
30
- if (dataProvider !== currentDataProvider) {
31
- throw new Error(
32
- 'Constant violation: The DataProvider provided to the useResource hook must not be replaced - Check your ConfigContext provider and the dataProvider property',
33
- );
34
- }
35
178
 
36
- const [instance, setInstance] = React.useState();
37
- React.useEffect(() => {
38
- const i = dataProvider.createInstance();
39
- setInstance(i);
40
- return () => {
41
- dataProvider.releaseInstance(i);
42
- };
43
- }, []);
44
-
45
- const nextRequestDetails = React.useMemo(
179
+ const nextRequestDetails = useMemo(
46
180
  () => ({ name: nextName, query: nextQuery, empty: nextEmpty, options: nextOptions }),
47
181
  [nextName, nextQuery, nextEmpty, nextOptions],
48
182
  );
49
- const [state, setState] = React.useState(() => {
183
+ const [state, dispatch] = useReducer(reducer, undefined, () => {
50
184
  const request = uniqueIdentifier();
51
185
  const revision = uniqueIdentifier();
52
- return {
186
+ return createRequestInstruction({
187
+ dataProvider: currentDataProvider,
53
188
  requestDetails: nextRequestDetails,
54
189
  request,
55
190
  revision,
@@ -68,165 +203,96 @@ function useResource({
68
203
  isInitial: !nextRequestDetails.empty,
69
204
  },
70
205
  persistent: nextPersistent,
71
- };
206
+ });
72
207
  });
73
- const { requestDetails, request, revision, isLoading, value, persistent } = state;
208
+ const {
209
+ dataProvider,
210
+ requestDetails,
211
+ request,
212
+ revision,
213
+ isLoading,
214
+ value,
215
+ persistent,
216
+ requestInstruction,
217
+ } = state;
218
+
219
+ if (dataProvider == null) {
220
+ throw new Error(
221
+ 'Unmet requirement: The DataProvider for the useResource hook is missing - Check your ConfigContext provider and the dataProvider property',
222
+ );
223
+ }
224
+ if (dataProvider !== currentDataProvider) {
225
+ throw new Error(
226
+ 'Constant violation: The DataProvider provided to the useResource hook must not be replaced - Check your ConfigContext provider and the dataProvider property',
227
+ );
228
+ }
229
+
230
+ const [instance, setInstance] = useState();
231
+ useEffect(() => {
232
+ const i = dataProvider.createInstance();
233
+ setInstance(i ?? {});
234
+ return () => {
235
+ dataProvider.releaseInstance(i);
236
+ };
237
+ }, [dataProvider]);
74
238
 
75
239
  if (
76
240
  requestDetails !== nextRequestDetails &&
77
241
  !dataProvider.compareRequests(nextRequestDetails, requestDetails)
78
242
  ) {
79
- setState((prevState) => {
80
- const nextRequest = uniqueIdentifier(prevState.request);
81
- const nextRevision = uniqueIdentifier(prevState.revision);
82
- let isPersistent = false;
83
- if (prevState.persistent === 'very' && nextPersistent === 'very') {
84
- isPersistent = 'very';
85
- } else if (prevState.persistent && nextPersistent) {
86
- isPersistent = true;
87
- }
88
- const shouldValuePersist =
89
- !nextRequestDetails.empty &&
90
- dataProvider.shouldPersist(
91
- nextRequestDetails,
92
- prevState.requestDetails,
93
- isPersistent,
94
- prevState.value,
95
- );
96
- return {
97
- requestDetails: nextRequestDetails,
98
- request: nextRequest,
99
- revision: nextRevision,
100
- isLoading: !nextRequestDetails.empty,
101
- value: shouldValuePersist
102
- ? prevState.value
103
- : {
104
- name: nextRequestDetails.name,
105
- query: nextRequestDetails.query,
106
- options: nextRequestDetails.options,
107
- request: nextRequest,
108
- revision: nextRevision,
109
- data: [],
110
- meta: {},
111
- error: undefined,
112
- isEmpty: !!nextRequestDetails.empty,
113
- isIncomplete: !nextRequestDetails.empty,
114
- isInitial: !nextRequestDetails.empty,
115
- },
116
- persistent: nextPersistent,
117
- };
243
+ dispatch({
244
+ type: 'next-request',
245
+ requestDetails: nextRequestDetails,
246
+ persistent: nextPersistent,
118
247
  });
119
248
  } else if (persistent !== nextPersistent) {
120
- setState((prevState) => ({ ...prevState, persistent: nextPersistent }));
249
+ dispatch({ type: 'set-persistence', persistent: nextPersistent });
121
250
  }
122
251
 
123
- const notify = React.useCallback(
252
+ const notify = useCallback(
124
253
  async () =>
125
254
  new Promise((resolve) => {
126
- setState((currentState) => {
127
- const nextRevision = uniqueIdentifier(currentState.revision);
128
- resolve({ request: currentState.request, revision: nextRevision });
129
- return { ...currentState, isLoading: true, revision: nextRevision };
130
- });
255
+ dispatch({ type: 'next-revision', notify: resolve });
131
256
  }),
132
257
  [],
133
258
  );
134
259
 
135
260
  // DataProvider events
136
- React.useEffect(() => {
261
+ useEffect(() => {
137
262
  if (requestDetails.empty) return undefined;
138
263
 
139
264
  const unsubscribe = dataProvider.subscribe(requestDetails.name, notify);
140
265
  return unsubscribe;
141
266
  }, [requestDetails.empty, dataProvider, requestDetails.name, notify]);
142
267
 
143
- React.useEffect(() => {
144
- if (requestDetails.empty || instance == null) return undefined;
268
+ // Fetch data when instructed
269
+ useEffect(() => {
270
+ if (instance == null || requestInstruction.requestDetails.empty) return undefined;
145
271
 
146
272
  const abortSignal = new AbortSignal();
147
273
 
148
- const meta = new Meta({ ...value.meta }, instance);
149
-
150
- let promise = Promise.resolve(state);
151
-
152
- const callback = (error, done, data) => {
153
- promise = promise.then((prevState) => {
154
- try {
155
- let nextState;
156
-
157
- if (error != null) {
158
- nextState = {
159
- ...prevState,
160
- isLoading: false,
161
- value: {
162
- ...prevState.value,
163
- error,
164
- isIncomplete: false,
165
- },
166
- };
167
- } else {
168
- const context = {
169
- name: requestDetails.name,
170
- query: requestDetails.query,
171
- options: requestDetails.options,
172
- request,
173
- revision,
174
- data,
175
- meta: meta.commit(prevState.value.meta),
176
- error: undefined,
177
- isEmpty: false,
178
- isIncomplete: !done,
179
- isInitial: !!prevState.isInitial && !done,
180
- };
181
- context.data = dataProvider.transition(context, prevState.value);
182
- context.data = dataProvider.recycleItems(context, prevState.value);
183
-
184
- nextState = {
185
- ...prevState,
186
- isLoading: !done,
187
- value: context,
188
- };
189
- }
190
-
191
- setState((otherState) => {
192
- if (request !== otherState.request || revision !== otherState.revision) {
193
- return otherState;
194
- }
195
-
196
- return nextState;
197
- });
198
-
199
- return nextState;
200
- } catch {
201
- return prevState;
202
- }
203
- });
204
- };
205
-
206
- dataProvider.continuousGet(
207
- requestDetails.name,
208
- requestDetails.query,
209
- requestDetails.options,
210
- meta,
211
- callback,
212
- abortSignal,
213
- );
274
+ // Start fetching data.
275
+ fetchData(requestInstruction, instance, abortSignal, dispatch);
214
276
 
215
277
  return () => {
278
+ // Abort fetching data when another request is pending or the React component is unmounted.
216
279
  abortSignal.abort();
217
280
  };
218
- }, [instance, request, revision]);
281
+ }, [instance, requestInstruction]);
219
282
 
220
283
  const isStale = revision !== value.revision;
221
- const next = React.useMemo(
222
- () => (isStale ? { request, revision } : { request: value.request, revision: value.revision }),
223
- [isStale, request, revision, value.request, value.revision],
284
+ const nextRequest = isStale ? request : value.request;
285
+ const nextRevision = isStale ? revision : value.revision;
286
+ const next = useMemo(
287
+ () => ({ request: nextRequest, revision: nextRevision }),
288
+ [nextRequest, nextRevision],
224
289
  );
225
- const context = React.useMemo(
290
+ const context = useMemo(
226
291
  () => ({ ...value, dataProvider, isLoading, isStale, next, notify }),
227
292
  [value, dataProvider, isLoading, isStale, next, notify],
228
293
  );
229
294
 
295
+ // Apply context plugins and return the final context.
230
296
  return dataProvider.contextPlugins.reduce((result, fn) => fn(result, rest), context);
231
297
  }
232
298