@civet/core 1.2.7 → 1.3.1

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,38 +1,39 @@
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 = {
9
- /** DataProvider to be used for requests */
7
+ /** DataProvider to be used for requests - must not be changed */
10
8
  dataProvider: dataProviderPropType,
11
9
  /** Resource name */
12
10
  name: PropTypes.string.isRequired,
13
- /** Query */
11
+ /** Query instructions */
14
12
  query: PropTypes.any,
15
- /** Whether to prevent fetching data */
13
+ /** Disables fetching data, resulting in an empty data array */
16
14
  empty: PropTypes.bool,
17
- /** DataProvider options for requests */
15
+ /** Query options for requests */
18
16
  options: PropTypes.object,
19
- /** Whether stale data should be retained during the next request - this only applies if neither dataProvider nor name have changed, unless set to "very" */
17
+ /** Whether stale data should be retained during the next request - this only applies if name did not change, unless set to "very" */
20
18
  persistent: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['very'])]),
21
19
  children: PropTypes.node,
22
20
  };
23
21
 
24
22
  /**
25
- * Makes data from an DataProvider available to its descendants using React's context API.
26
- * If not explicitly specified, necessary configuration is taken from the nearest <ConfigProvider>.
27
- * The provided DataProvider must not be replaced.
23
+ * Provides data based on the given request details and DataProvider.
24
+ * Context provider for the ResourceContext.
25
+ *
26
+ * Necessary configuration that is not directly specified is taken from the ConfigContext.
27
+ *
28
+ * The provided DataProvider must not be changed.
28
29
  */
29
30
  function Resource({ dataProvider, name, query, empty, options, persistent, children, ...rest }) {
30
- const context = useResource({ dataProvider, name, query, empty, options, persistent });
31
+ const context = useResource({ dataProvider, name, query, empty, options, persistent, ...rest });
31
32
 
32
33
  return context.dataProvider.uiPlugins.reduceRight(
33
- (next, Plugin) => (result) =>
34
- (
35
- // eslint-disable-next-line react/jsx-props-no-spreading
34
+ (next, Plugin) =>
35
+ // eslint-disable-next-line react/display-name
36
+ (result) => (
36
37
  <Plugin {...rest} context={result}>
37
38
  {next}
38
39
  </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,55 +1,198 @@
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.
10
- * If not explicitly specified, necessary configuration is taken from the nearest <ConfigProvider>.
11
- * The provided DataProvider must not be replaced.
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
+ * Provides data based on the given request details and DataProvider.
164
+ *
165
+ * Necessary configuration that is not directly specified is taken from the ConfigContext.
166
+ *
167
+ * The provided DataProvider must not be changed.
12
168
  */
13
169
  function useResource({
170
+ /** DataProvider to be used for requests - must not be changed */
14
171
  dataProvider: dataProviderProp,
172
+ /** Resource name */
15
173
  name: nextName,
174
+ /** Query instructions */
16
175
  query: nextQuery,
176
+ /** Disables fetching data, resulting in an empty data array */
17
177
  empty: nextEmpty,
178
+ /** Query options for requests */
18
179
  options: nextOptions,
180
+ /** Whether stale data should be retained during the next request - this only applies if name did not change, unless set to "very" */
19
181
  persistent: nextPersistent,
20
182
  ...rest
21
183
  }) {
22
184
  const configContext = useConfigContext();
23
185
  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
186
 
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(
187
+ const nextRequestDetails = useMemo(
46
188
  () => ({ name: nextName, query: nextQuery, empty: nextEmpty, options: nextOptions }),
47
189
  [nextName, nextQuery, nextEmpty, nextOptions],
48
190
  );
49
- const [state, setState] = React.useState(() => {
191
+ const [state, dispatch] = useReducer(reducer, undefined, () => {
50
192
  const request = uniqueIdentifier();
51
193
  const revision = uniqueIdentifier();
52
- return {
194
+ return createRequestInstruction({
195
+ dataProvider: currentDataProvider,
53
196
  requestDetails: nextRequestDetails,
54
197
  request,
55
198
  revision,
@@ -68,165 +211,96 @@ function useResource({
68
211
  isInitial: !nextRequestDetails.empty,
69
212
  },
70
213
  persistent: nextPersistent,
71
- };
214
+ });
72
215
  });
73
- const { requestDetails, request, revision, isLoading, value, persistent } = state;
216
+ const {
217
+ dataProvider,
218
+ requestDetails,
219
+ request,
220
+ revision,
221
+ isLoading,
222
+ value,
223
+ persistent,
224
+ requestInstruction,
225
+ } = state;
226
+
227
+ if (dataProvider == null) {
228
+ throw new Error(
229
+ 'Unmet requirement: The DataProvider for the useResource hook is missing - Check your ConfigContext provider and the dataProvider property',
230
+ );
231
+ }
232
+ if (dataProvider !== currentDataProvider) {
233
+ throw new Error(
234
+ 'Constant violation: The DataProvider provided to the useResource hook must not be replaced - Check your ConfigContext provider and the dataProvider property',
235
+ );
236
+ }
237
+
238
+ const [instance, setInstance] = useState();
239
+ useEffect(() => {
240
+ const i = dataProvider.createInstance();
241
+ setInstance(i ?? {});
242
+ return () => {
243
+ dataProvider.releaseInstance(i);
244
+ };
245
+ }, [dataProvider]);
74
246
 
75
247
  if (
76
248
  requestDetails !== nextRequestDetails &&
77
249
  !dataProvider.compareRequests(nextRequestDetails, requestDetails)
78
250
  ) {
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
- };
251
+ dispatch({
252
+ type: 'next-request',
253
+ requestDetails: nextRequestDetails,
254
+ persistent: nextPersistent,
118
255
  });
119
256
  } else if (persistent !== nextPersistent) {
120
- setState((prevState) => ({ ...prevState, persistent: nextPersistent }));
257
+ dispatch({ type: 'set-persistence', persistent: nextPersistent });
121
258
  }
122
259
 
123
- const notify = React.useCallback(
260
+ const notify = useCallback(
124
261
  async () =>
125
262
  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
- });
263
+ dispatch({ type: 'next-revision', notify: resolve });
131
264
  }),
132
265
  [],
133
266
  );
134
267
 
135
268
  // DataProvider events
136
- React.useEffect(() => {
269
+ useEffect(() => {
137
270
  if (requestDetails.empty) return undefined;
138
271
 
139
272
  const unsubscribe = dataProvider.subscribe(requestDetails.name, notify);
140
273
  return unsubscribe;
141
274
  }, [requestDetails.empty, dataProvider, requestDetails.name, notify]);
142
275
 
143
- React.useEffect(() => {
144
- if (requestDetails.empty || instance == null) return undefined;
276
+ // Fetch data when instructed
277
+ useEffect(() => {
278
+ if (instance == null || requestInstruction.requestDetails.empty) return undefined;
145
279
 
146
280
  const abortSignal = new AbortSignal();
147
281
 
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
- );
282
+ // Start fetching data.
283
+ fetchData(requestInstruction, instance, abortSignal, dispatch);
214
284
 
215
285
  return () => {
286
+ // Abort fetching data when another request is pending or the React component is unmounted.
216
287
  abortSignal.abort();
217
288
  };
218
- }, [instance, request, revision]);
289
+ }, [instance, requestInstruction]);
219
290
 
220
291
  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],
292
+ const nextRequest = isStale ? request : value.request;
293
+ const nextRevision = isStale ? revision : value.revision;
294
+ const next = useMemo(
295
+ () => ({ request: nextRequest, revision: nextRevision }),
296
+ [nextRequest, nextRevision],
224
297
  );
225
- const context = React.useMemo(
298
+ const context = useMemo(
226
299
  () => ({ ...value, dataProvider, isLoading, isStale, next, notify }),
227
300
  [value, dataProvider, isLoading, isStale, next, notify],
228
301
  );
229
302
 
303
+ // Apply context plugins and return the final context.
230
304
  return dataProvider.contextPlugins.reduce((result, fn) => fn(result, rest), context);
231
305
  }
232
306