@data-client/core 0.1.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 (308) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +71 -0
  3. package/dist/index.js +1643 -0
  4. package/dist/index.umd.min.js +1 -0
  5. package/dist/next.js +461 -0
  6. package/dist/package.json +1 -0
  7. package/legacy/actionTypes.js +12 -0
  8. package/legacy/compatibleActions.js +2 -0
  9. package/legacy/controller/BaseController.js +289 -0
  10. package/legacy/controller/Controller.js +20 -0
  11. package/legacy/controller/createFetch.js +42 -0
  12. package/legacy/controller/createInvalidate.js +12 -0
  13. package/legacy/controller/createInvalidateAll.js +8 -0
  14. package/legacy/controller/createOptimistic.js +33 -0
  15. package/legacy/controller/createReceive.js +36 -0
  16. package/legacy/controller/createReset.js +8 -0
  17. package/legacy/controller/createSubscription.js +30 -0
  18. package/legacy/controller/types.js +2 -0
  19. package/legacy/endpoint/index.js +2 -0
  20. package/legacy/endpoint/shapes.js +2 -0
  21. package/legacy/endpoint/types.js +2 -0
  22. package/legacy/fsa.js +2 -0
  23. package/legacy/index.js +22 -0
  24. package/legacy/internal.js +4 -0
  25. package/legacy/legacyActions.js +2 -0
  26. package/legacy/manager/ConnectionListener.js +2 -0
  27. package/legacy/manager/DefaultConnectionListener.js +40 -0
  28. package/legacy/manager/DevtoolsManager.js +73 -0
  29. package/legacy/manager/LogoutManager.js +34 -0
  30. package/legacy/manager/NetworkManager.js +291 -0
  31. package/legacy/manager/PollingSubscription.js +159 -0
  32. package/legacy/manager/SubscriptionManager.js +117 -0
  33. package/legacy/manager/applyManager.js +23 -0
  34. package/legacy/manager/devtoolsTypes.js +2 -0
  35. package/legacy/manager/index.js +6 -0
  36. package/legacy/middlewareTypes.js +2 -0
  37. package/legacy/newActions.js +2 -0
  38. package/legacy/next/Controller.js +24 -0
  39. package/legacy/next/index.js +3 -0
  40. package/legacy/previousActions.js +2 -0
  41. package/legacy/state/RIC.js +3 -0
  42. package/legacy/state/applyUpdatersToResults.js +4 -0
  43. package/legacy/state/legacy-actions/createFetch.js +62 -0
  44. package/legacy/state/legacy-actions/createReceive.js +37 -0
  45. package/legacy/state/legacy-actions/createReceiveError.js +28 -0
  46. package/legacy/state/legacy-actions/index.js +4 -0
  47. package/legacy/state/reducer/createReducer.js +54 -0
  48. package/legacy/state/reducer/fetchReducer.js +32 -0
  49. package/legacy/state/reducer/invalidateReducer.js +28 -0
  50. package/legacy/state/reducer/setReducer.js +113 -0
  51. package/legacy/state/reducerInstance.js +9 -0
  52. package/legacy/state/selectMeta.js +4 -0
  53. package/legacy/types.js +8 -0
  54. package/lib/actionTypes.d.ts +11 -0
  55. package/lib/actionTypes.d.ts.map +1 -0
  56. package/lib/actionTypes.js +12 -0
  57. package/lib/compatibleActions.d.ts +47 -0
  58. package/lib/compatibleActions.d.ts.map +1 -0
  59. package/lib/compatibleActions.js +2 -0
  60. package/lib/controller/BaseController.d.ts +128 -0
  61. package/lib/controller/BaseController.d.ts.map +1 -0
  62. package/lib/controller/BaseController.js +289 -0
  63. package/lib/controller/Controller.d.ts +14 -0
  64. package/lib/controller/Controller.d.ts.map +1 -0
  65. package/lib/controller/Controller.js +20 -0
  66. package/lib/controller/createFetch.d.ts +12 -0
  67. package/lib/controller/createFetch.d.ts.map +1 -0
  68. package/lib/controller/createFetch.js +42 -0
  69. package/lib/controller/createInvalidate.d.ts +6 -0
  70. package/lib/controller/createInvalidate.d.ts.map +1 -0
  71. package/lib/controller/createInvalidate.js +12 -0
  72. package/lib/controller/createInvalidateAll.d.ts +3 -0
  73. package/lib/controller/createInvalidateAll.d.ts.map +1 -0
  74. package/lib/controller/createInvalidateAll.js +8 -0
  75. package/lib/controller/createOptimistic.d.ts +10 -0
  76. package/lib/controller/createOptimistic.d.ts.map +1 -0
  77. package/lib/controller/createOptimistic.js +33 -0
  78. package/lib/controller/createReceive.d.ts +20 -0
  79. package/lib/controller/createReceive.d.ts.map +1 -0
  80. package/lib/controller/createReceive.js +36 -0
  81. package/lib/controller/createReset.d.ts +3 -0
  82. package/lib/controller/createReset.d.ts.map +1 -0
  83. package/lib/controller/createReset.js +8 -0
  84. package/lib/controller/createSubscription.d.ts +9 -0
  85. package/lib/controller/createSubscription.d.ts.map +1 -0
  86. package/lib/controller/createSubscription.js +30 -0
  87. package/lib/controller/types.d.ts +6 -0
  88. package/lib/controller/types.d.ts.map +1 -0
  89. package/lib/controller/types.js +2 -0
  90. package/lib/endpoint/index.d.ts +3 -0
  91. package/lib/endpoint/index.d.ts.map +1 -0
  92. package/lib/endpoint/index.js +2 -0
  93. package/lib/endpoint/shapes.d.ts +25 -0
  94. package/lib/endpoint/shapes.d.ts.map +1 -0
  95. package/lib/endpoint/shapes.js +2 -0
  96. package/lib/endpoint/types.d.ts +45 -0
  97. package/lib/endpoint/types.d.ts.map +1 -0
  98. package/lib/endpoint/types.js +2 -0
  99. package/lib/fsa.d.ts +41 -0
  100. package/lib/fsa.d.ts.map +1 -0
  101. package/lib/fsa.js +2 -0
  102. package/lib/index.d.ts +19 -0
  103. package/lib/index.d.ts.map +1 -0
  104. package/lib/index.js +22 -0
  105. package/lib/internal.d.ts +4 -0
  106. package/lib/internal.d.ts.map +1 -0
  107. package/lib/internal.js +4 -0
  108. package/lib/legacyActions.d.ts +92 -0
  109. package/lib/legacyActions.d.ts.map +1 -0
  110. package/lib/legacyActions.js +2 -0
  111. package/lib/manager/ConnectionListener.d.ts +8 -0
  112. package/lib/manager/ConnectionListener.d.ts.map +1 -0
  113. package/lib/manager/ConnectionListener.js +2 -0
  114. package/lib/manager/DefaultConnectionListener.d.ts +20 -0
  115. package/lib/manager/DefaultConnectionListener.d.ts.map +1 -0
  116. package/lib/manager/DefaultConnectionListener.js +40 -0
  117. package/lib/manager/DevtoolsManager.d.ts +24 -0
  118. package/lib/manager/DevtoolsManager.d.ts.map +1 -0
  119. package/lib/manager/DevtoolsManager.js +74 -0
  120. package/lib/manager/LogoutManager.d.ts +25 -0
  121. package/lib/manager/LogoutManager.d.ts.map +1 -0
  122. package/lib/manager/LogoutManager.js +34 -0
  123. package/lib/manager/NetworkManager.d.ts +82 -0
  124. package/lib/manager/NetworkManager.d.ts.map +1 -0
  125. package/lib/manager/NetworkManager.js +295 -0
  126. package/lib/manager/PollingSubscription.d.ts +45 -0
  127. package/lib/manager/PollingSubscription.d.ts.map +1 -0
  128. package/lib/manager/PollingSubscription.js +159 -0
  129. package/lib/manager/SubscriptionManager.d.ts +55 -0
  130. package/lib/manager/SubscriptionManager.d.ts.map +1 -0
  131. package/lib/manager/SubscriptionManager.js +117 -0
  132. package/lib/manager/applyManager.d.ts +10 -0
  133. package/lib/manager/applyManager.d.ts.map +1 -0
  134. package/lib/manager/applyManager.js +23 -0
  135. package/lib/manager/devtoolsTypes.d.ts +205 -0
  136. package/lib/manager/devtoolsTypes.d.ts.map +1 -0
  137. package/lib/manager/devtoolsTypes.js +2 -0
  138. package/lib/manager/index.d.ts +8 -0
  139. package/lib/manager/index.d.ts.map +1 -0
  140. package/lib/manager/index.js +6 -0
  141. package/lib/middlewareTypes.d.ts +18 -0
  142. package/lib/middlewareTypes.d.ts.map +1 -0
  143. package/lib/middlewareTypes.js +2 -0
  144. package/lib/newActions.d.ts +85 -0
  145. package/lib/newActions.d.ts.map +1 -0
  146. package/lib/newActions.js +2 -0
  147. package/lib/next/Controller.d.ts +14 -0
  148. package/lib/next/Controller.d.ts.map +1 -0
  149. package/lib/next/Controller.js +24 -0
  150. package/lib/next/index.d.ts +3 -0
  151. package/lib/next/index.d.ts.map +1 -0
  152. package/lib/next/index.js +3 -0
  153. package/lib/previousActions.d.ts +91 -0
  154. package/lib/previousActions.d.ts.map +1 -0
  155. package/lib/previousActions.js +2 -0
  156. package/lib/state/RIC.d.ts +2 -0
  157. package/lib/state/RIC.js +3 -0
  158. package/lib/state/applyUpdatersToResults.d.ts +13 -0
  159. package/lib/state/applyUpdatersToResults.d.ts.map +1 -0
  160. package/lib/state/applyUpdatersToResults.js +7 -0
  161. package/lib/state/legacy-actions/createFetch.d.ts +19 -0
  162. package/lib/state/legacy-actions/createFetch.d.ts.map +1 -0
  163. package/lib/state/legacy-actions/createFetch.js +62 -0
  164. package/lib/state/legacy-actions/createReceive.d.ts +14 -0
  165. package/lib/state/legacy-actions/createReceive.d.ts.map +1 -0
  166. package/lib/state/legacy-actions/createReceive.js +37 -0
  167. package/lib/state/legacy-actions/createReceiveError.d.ts +9 -0
  168. package/lib/state/legacy-actions/createReceiveError.d.ts.map +1 -0
  169. package/lib/state/legacy-actions/createReceiveError.js +28 -0
  170. package/lib/state/legacy-actions/index.d.ts +4 -0
  171. package/lib/state/legacy-actions/index.d.ts.map +1 -0
  172. package/lib/state/legacy-actions/index.js +4 -0
  173. package/lib/state/reducer/createReducer.d.ts +7 -0
  174. package/lib/state/reducer/createReducer.d.ts.map +1 -0
  175. package/lib/state/reducer/createReducer.js +55 -0
  176. package/lib/state/reducer/fetchReducer.d.ts +4 -0
  177. package/lib/state/reducer/fetchReducer.d.ts.map +1 -0
  178. package/lib/state/reducer/fetchReducer.js +34 -0
  179. package/lib/state/reducer/invalidateReducer.d.ts +37 -0
  180. package/lib/state/reducer/invalidateReducer.d.ts.map +1 -0
  181. package/lib/state/reducer/invalidateReducer.js +34 -0
  182. package/lib/state/reducer/setReducer.d.ts +40 -0
  183. package/lib/state/reducer/setReducer.d.ts.map +1 -0
  184. package/lib/state/reducer/setReducer.js +119 -0
  185. package/lib/state/reducerInstance.d.ts +7 -0
  186. package/lib/state/reducerInstance.d.ts.map +1 -0
  187. package/lib/state/reducerInstance.js +9 -0
  188. package/lib/state/selectMeta.d.ts +3 -0
  189. package/lib/state/selectMeta.d.ts.map +1 -0
  190. package/lib/state/selectMeta.js +4 -0
  191. package/lib/types.d.ts +71 -0
  192. package/lib/types.d.ts.map +1 -0
  193. package/lib/types.js +8 -0
  194. package/node.mjs +1 -0
  195. package/package.json +127 -0
  196. package/src/actionTypes.ts +11 -0
  197. package/src/compatibleActions.ts +96 -0
  198. package/src/controller/BaseController.ts +508 -0
  199. package/src/controller/Controller.ts +32 -0
  200. package/src/controller/__tests__/Controller.ts +29 -0
  201. package/src/controller/__tests__/__snapshots__/getResponse.ts.snap +35 -0
  202. package/src/controller/__tests__/getResponse.ts +182 -0
  203. package/src/controller/createFetch.ts +54 -0
  204. package/src/controller/createInvalidate.ts +16 -0
  205. package/src/controller/createInvalidateAll.ts +11 -0
  206. package/src/controller/createOptimistic.ts +47 -0
  207. package/src/controller/createReceive.ts +85 -0
  208. package/src/controller/createReset.ts +9 -0
  209. package/src/controller/createSubscription.ts +39 -0
  210. package/src/controller/types.ts +22 -0
  211. package/src/endpoint/index.ts +14 -0
  212. package/src/endpoint/shapes.ts +53 -0
  213. package/src/endpoint/types.ts +72 -0
  214. package/src/fsa.ts +99 -0
  215. package/src/index.ts +61 -0
  216. package/src/internal.ts +3 -0
  217. package/src/legacyActions.ts +163 -0
  218. package/src/manager/ConnectionListener.ts +7 -0
  219. package/src/manager/DefaultConnectionListener.ts +54 -0
  220. package/src/manager/DevtoolsManager.ts +99 -0
  221. package/src/manager/LogoutManager.ts +57 -0
  222. package/src/manager/NetworkManager.ts +346 -0
  223. package/src/manager/PollingSubscription.ts +190 -0
  224. package/src/manager/SubscriptionManager.ts +156 -0
  225. package/src/manager/__tests__/__snapshots__/pollingSubscription-endpoint.ts.snap +49 -0
  226. package/src/manager/__tests__/__snapshots__/pollingSubscription.ts.snap +43 -0
  227. package/src/manager/__tests__/logoutManager.ts +112 -0
  228. package/src/manager/__tests__/manager.ts +44 -0
  229. package/src/manager/__tests__/networkManager-legacy.ts +394 -0
  230. package/src/manager/__tests__/networkManager.ts +426 -0
  231. package/src/manager/__tests__/pollingSubscription-endpoint.ts +423 -0
  232. package/src/manager/__tests__/pollingSubscription.ts +313 -0
  233. package/src/manager/__tests__/subscriptionManager.ts +208 -0
  234. package/src/manager/applyManager.ts +33 -0
  235. package/src/manager/devtoolsTypes.ts +210 -0
  236. package/src/manager/index.ts +7 -0
  237. package/src/middlewareTypes.ts +49 -0
  238. package/src/newActions.ts +140 -0
  239. package/src/next/Controller.ts +39 -0
  240. package/src/next/index.ts +2 -0
  241. package/src/package.json +1 -0
  242. package/src/previousActions.ts +159 -0
  243. package/src/state/RIC.d.ts +2 -0
  244. package/src/state/RIC.js +5 -0
  245. package/src/state/__tests__/RIC.web.ts +16 -0
  246. package/src/state/__tests__/__snapshots__/reducer.ts.snap +56 -0
  247. package/src/state/__tests__/applyUpdatersToResults.ts +40 -0
  248. package/src/state/__tests__/reducer.ts +868 -0
  249. package/src/state/applyUpdatersToResults.ts +29 -0
  250. package/src/state/legacy-actions/createFetch.ts +95 -0
  251. package/src/state/legacy-actions/createReceive.ts +68 -0
  252. package/src/state/legacy-actions/createReceiveError.ts +43 -0
  253. package/src/state/legacy-actions/index.ts +3 -0
  254. package/src/state/reducer/createReducer.ts +80 -0
  255. package/src/state/reducer/fetchReducer.ts +48 -0
  256. package/src/state/reducer/invalidateReducer.ts +39 -0
  257. package/src/state/reducer/setReducer.ts +157 -0
  258. package/src/state/reducerInstance.ts +14 -0
  259. package/src/state/selectMeta.ts +8 -0
  260. package/src/types.ts +125 -0
  261. package/ts3.4/actionTypes.d.ts +11 -0
  262. package/ts3.4/compatibleActions.d.ts +47 -0
  263. package/ts3.4/controller/BaseController.d.ts +170 -0
  264. package/ts3.4/controller/Controller.d.ts +14 -0
  265. package/ts3.4/controller/createFetch.d.ts +14 -0
  266. package/ts3.4/controller/createInvalidate.d.ts +8 -0
  267. package/ts3.4/controller/createInvalidateAll.d.ts +3 -0
  268. package/ts3.4/controller/createOptimistic.d.ts +12 -0
  269. package/ts3.4/controller/createReceive.d.ts +24 -0
  270. package/ts3.4/controller/createReset.d.ts +3 -0
  271. package/ts3.4/controller/createSubscription.d.ts +13 -0
  272. package/ts3.4/controller/types.d.ts +6 -0
  273. package/ts3.4/endpoint/index.d.ts +3 -0
  274. package/ts3.4/endpoint/shapes.d.ts +25 -0
  275. package/ts3.4/endpoint/types.d.ts +45 -0
  276. package/ts3.4/fsa.d.ts +41 -0
  277. package/ts3.4/index.d.ts +22 -0
  278. package/ts3.4/internal.d.ts +4 -0
  279. package/ts3.4/legacyActions.d.ts +95 -0
  280. package/ts3.4/manager/ConnectionListener.d.ts +8 -0
  281. package/ts3.4/manager/DefaultConnectionListener.d.ts +20 -0
  282. package/ts3.4/manager/DevtoolsManager.d.ts +24 -0
  283. package/ts3.4/manager/LogoutManager.d.ts +25 -0
  284. package/ts3.4/manager/NetworkManager.d.ts +82 -0
  285. package/ts3.4/manager/PollingSubscription.d.ts +45 -0
  286. package/ts3.4/manager/SubscriptionManager.d.ts +55 -0
  287. package/ts3.4/manager/applyManager.d.ts +10 -0
  288. package/ts3.4/manager/devtoolsTypes.d.ts +205 -0
  289. package/ts3.4/manager/index.d.ts +8 -0
  290. package/ts3.4/middlewareTypes.d.ts +18 -0
  291. package/ts3.4/newActions.d.ts +88 -0
  292. package/ts3.4/next/Controller.d.ts +14 -0
  293. package/ts3.4/next/index.d.ts +3 -0
  294. package/ts3.4/previousActions.d.ts +94 -0
  295. package/ts3.4/state/RIC.d.ts +2 -0
  296. package/ts3.4/state/applyUpdatersToResults.d.ts +13 -0
  297. package/ts3.4/state/legacy-actions/createFetch.d.ts +19 -0
  298. package/ts3.4/state/legacy-actions/createReceive.d.ts +14 -0
  299. package/ts3.4/state/legacy-actions/createReceiveError.d.ts +9 -0
  300. package/ts3.4/state/legacy-actions/index.d.ts +4 -0
  301. package/ts3.4/state/reducer/createReducer.d.ts +7 -0
  302. package/ts3.4/state/reducer/fetchReducer.d.ts +4 -0
  303. package/ts3.4/state/reducer/invalidateReducer.d.ts +37 -0
  304. package/ts3.4/state/reducer/setReducer.d.ts +40 -0
  305. package/ts3.4/state/reducerInstance.d.ts +7 -0
  306. package/ts3.4/state/selectMeta.d.ts +3 -0
  307. package/ts3.4/types.d.ts +73 -0
  308. package/typescript.svg +8 -0
@@ -0,0 +1,346 @@
1
+ import { SET_TYPE, FETCH_TYPE, RESET_TYPE } from '../actionTypes.js';
2
+ import Controller from '../controller/Controller.js';
3
+ import { initialState } from '../internal.js';
4
+ import {
5
+ createReceive,
6
+ createReceiveError,
7
+ } from '../state/legacy-actions/index.js';
8
+ import RIC from '../state/RIC.js';
9
+ import type {
10
+ FetchAction,
11
+ ReceiveAction,
12
+ Manager,
13
+ ActionTypes,
14
+ MiddlewareAPI,
15
+ Middleware,
16
+ State,
17
+ } from '../types.js';
18
+
19
+ export class ResetError extends Error {
20
+ name = 'ResetError';
21
+
22
+ constructor() {
23
+ super('Aborted due to RESET');
24
+ }
25
+ }
26
+
27
+ /** Handles all async network dispatches
28
+ *
29
+ * Dedupes concurrent requests by keeping track of all fetches in flight
30
+ * and returning existing promises for requests already in flight.
31
+ *
32
+ * Interfaces with store via a redux-compatible middleware.
33
+ *
34
+ * @see https://resthooks.io/docs/api/NetworkManager
35
+ */
36
+ export default class NetworkManager implements Manager {
37
+ protected fetched: { [k: string]: Promise<any> } = Object.create(null);
38
+ protected resolvers: { [k: string]: (value?: any) => void } = {};
39
+ protected rejectors: { [k: string]: (value?: any) => void } = {};
40
+ protected fetchedAt: { [k: string]: number } = {};
41
+ declare readonly dataExpiryLength: number;
42
+ declare readonly errorExpiryLength: number;
43
+ protected declare middleware: Middleware;
44
+ protected getState: () => State<unknown> = () => initialState;
45
+ protected controller: Controller = new Controller();
46
+ declare cleanupDate?: number;
47
+
48
+ constructor(dataExpiryLength = 60000, errorExpiryLength = 1000) {
49
+ this.dataExpiryLength = dataExpiryLength;
50
+ this.errorExpiryLength = errorExpiryLength;
51
+
52
+ this.middleware = <C extends MiddlewareAPI>({
53
+ dispatch,
54
+ getState,
55
+ controller,
56
+ }: C) => {
57
+ this.getState = getState;
58
+ this.controller = controller;
59
+ return (next: C['dispatch']): C['dispatch'] =>
60
+ (action): Promise<void> => {
61
+ switch (action.type) {
62
+ case FETCH_TYPE:
63
+ this.handleFetch(action, dispatch, controller);
64
+ // This is the only case that causes any state change
65
+ // It's important to intercept other fetches as we don't want to trigger reducers during
66
+ // render - so we need to stop 'readonly' fetches which can be triggered in render
67
+ if (
68
+ action.meta.optimisticResponse !== undefined ||
69
+ (action.endpoint?.getOptimisticResponse !== undefined &&
70
+ action.endpoint.sideEffect)
71
+ ) {
72
+ return next(action);
73
+ }
74
+ return Promise.resolve();
75
+ case SET_TYPE:
76
+ // only receive after new state is computed
77
+ return next(action).then(() => {
78
+ if (action.meta.key in this.fetched) {
79
+ // Note: meta *must* be set by reducer so this should be safe
80
+ const error =
81
+ controller.getState().meta[action.meta.key]?.error;
82
+ // processing errors result in state meta having error, so we should reject the promise
83
+ if (error) {
84
+ // TODO: use only new action types
85
+ this.handleReceive(createReceiveError(error, action.meta));
86
+ } else {
87
+ this.handleReceive(action);
88
+ }
89
+ }
90
+ });
91
+ case RESET_TYPE: {
92
+ const rejectors = { ...this.rejectors };
93
+
94
+ this.clearAll();
95
+ return next(action).then(() => {
96
+ // there could be external listeners to the promise
97
+ // this must happen after commit so our own rejector knows not to dispatch an error based on this
98
+ for (const k in rejectors) {
99
+ rejectors[k](new ResetError());
100
+ }
101
+ });
102
+ }
103
+ default:
104
+ return next(action);
105
+ }
106
+ };
107
+ };
108
+ }
109
+
110
+ /** Used by DevtoolsManager to determine whether to log an action */
111
+ skipLogging(action: ActionTypes) {
112
+ /* istanbul ignore next */
113
+ return action.type === FETCH_TYPE && action.meta.key in this.fetched;
114
+ }
115
+
116
+ /** On mount */
117
+ init() {
118
+ delete this.cleanupDate;
119
+ }
120
+
121
+ /** Ensures all promises are completed by rejecting remaining. */
122
+ cleanup() {
123
+ // ensure no dispatches after unmount
124
+ // this must be reversible (done in init) so useEffect() remains symmetric
125
+ this.cleanupDate = Date.now();
126
+ }
127
+
128
+ allSettled() {
129
+ const fetches = Object.values(this.fetched);
130
+ if (fetches.length) return Promise.allSettled(fetches);
131
+ }
132
+
133
+ /** Clear all promise state */
134
+ protected clearAll() {
135
+ for (const k in this.rejectors) {
136
+ this.clear(k);
137
+ }
138
+ }
139
+
140
+ /** Clear promise state for a given key */
141
+ protected clear(key: string) {
142
+ this.fetched[key].catch(() => {});
143
+ delete this.resolvers[key];
144
+ delete this.rejectors[key];
145
+ delete this.fetched[key];
146
+ delete this.fetchedAt[key];
147
+ }
148
+
149
+ protected getLastReset() {
150
+ if (this.cleanupDate) return this.cleanupDate;
151
+ const lastReset = this.controller.getState().lastReset;
152
+ if (lastReset instanceof Date) return lastReset.valueOf();
153
+ if (typeof lastReset !== 'number') return -Infinity;
154
+ return lastReset;
155
+ }
156
+
157
+ /** Called when middleware intercepts 'rest-hooks/fetch' action.
158
+ *
159
+ * Will then start a promise for a key and potentially start the network
160
+ * fetch.
161
+ *
162
+ * Uses throttle only when instructed by action meta. This is valuable
163
+ * for ensures mutation requests always go through.
164
+ */
165
+ protected handleFetch(
166
+ action: FetchAction,
167
+ dispatch: (action: any) => Promise<void>,
168
+ controller: Controller,
169
+ ) {
170
+ const fetch = action.payload;
171
+ const { key, throttle, resolve, reject } = action.meta;
172
+ // TODO(breaking): remove support for Date type in 'Receive' action
173
+ const createdAt =
174
+ typeof action.meta.createdAt !== 'number'
175
+ ? action.meta.createdAt.getTime()
176
+ : action.meta.createdAt;
177
+
178
+ const deferedFetch = () => {
179
+ let promise = fetch();
180
+ const resolvePromise = (
181
+ promise: Promise<string | number | object | null>,
182
+ ) =>
183
+ promise
184
+ .then(data => {
185
+ resolve(data);
186
+ return data;
187
+ })
188
+ .catch(error => {
189
+ reject(error);
190
+ throw error;
191
+ });
192
+ // schedule non-throttled resolutions in a microtask before receive
193
+ // this enables users awaiting their fetch to trigger any react updates needed to deal
194
+ // with upcoming changes because of the fetch (for instance avoiding suspense if something is deleted)
195
+ if (!throttle && action.endpoint) {
196
+ promise = resolvePromise(promise);
197
+ }
198
+ promise = promise
199
+ .then(data => {
200
+ let lastReset = this.getLastReset();
201
+
202
+ /* istanbul ignore else */
203
+ if (process.env.NODE_ENV !== 'production' && isNaN(lastReset)) {
204
+ console.error(
205
+ 'state.lastReset is NaN. Only positive timestamps are valid.',
206
+ );
207
+ lastReset = 0;
208
+ }
209
+
210
+ // don't update state with promises started before last clear
211
+ if (createdAt >= lastReset) {
212
+ // we still check for controller in case someone didn't have type protection since this didn't always exist
213
+ if (action.endpoint && this.controller) {
214
+ this.controller.resolve(action.endpoint, {
215
+ args: action.meta.args as any,
216
+ response: data,
217
+ fetchedAt: createdAt,
218
+ });
219
+ } else {
220
+ // TODO(breaking): is this branch still possible? remove in next major update
221
+ // does this throw if the reducer fails? - no because reducer is wrapped in try/catch
222
+ this.controller.dispatch(
223
+ createReceive(data, {
224
+ ...action.meta,
225
+ fetchedAt: createdAt,
226
+ dataExpiryLength:
227
+ action.meta.options?.dataExpiryLength ??
228
+ this.dataExpiryLength,
229
+ }),
230
+ );
231
+ }
232
+ }
233
+ return data;
234
+ })
235
+ .catch(error => {
236
+ const lastReset = this.getLastReset();
237
+ // don't update state with promises started before last clear
238
+ if (createdAt >= lastReset) {
239
+ if (action.endpoint && this.controller) {
240
+ this.controller.resolve(action.endpoint, {
241
+ args: action.meta.args as any,
242
+ response: error,
243
+ fetchedAt: createdAt,
244
+ error: true,
245
+ });
246
+ } else {
247
+ this.controller.dispatch(
248
+ createReceiveError(error, {
249
+ ...action.meta,
250
+ errorExpiryLength:
251
+ action.meta.options?.errorExpiryLength ??
252
+ this.errorExpiryLength,
253
+ fetchedAt: createdAt,
254
+ }),
255
+ );
256
+ }
257
+ }
258
+ throw error;
259
+ });
260
+ // legacy behavior schedules resolution after dispatch
261
+ if (!throttle && !action.endpoint) {
262
+ promise = resolvePromise(promise);
263
+ }
264
+ return promise;
265
+ };
266
+
267
+ if (throttle) {
268
+ return this.throttle(key, deferedFetch, createdAt)
269
+ .then(data => resolve(data))
270
+ .catch(error => reject(error));
271
+ } else {
272
+ return deferedFetch().catch(() => {});
273
+ }
274
+ }
275
+
276
+ /** Called when middleware intercepts a receive action.
277
+ *
278
+ * Will resolve the promise associated with receive key.
279
+ */
280
+ protected handleReceive(action: ReceiveAction) {
281
+ // this can still turn out to be untrue since this is async
282
+ if (action.meta.key in this.fetched) {
283
+ let promiseHandler: (value?: any) => void;
284
+ if (action.error) {
285
+ promiseHandler = this.rejectors[action.meta.key];
286
+ } else {
287
+ promiseHandler = this.resolvers[action.meta.key];
288
+ }
289
+ promiseHandler(action.payload);
290
+ // since we're resolved we no longer need to keep track of this promise
291
+ this.clear(action.meta.key);
292
+ }
293
+ }
294
+
295
+ /** Attaches NetworkManager to store
296
+ *
297
+ * Intercepts 'rest-hooks/fetch' actions to start requests.
298
+ *
299
+ * Resolve/rejects a request when matching 'rest-hooks/receive' event
300
+ * is seen.
301
+ */
302
+ getMiddleware() {
303
+ return this.middleware;
304
+ }
305
+
306
+ /** Ensures only one request for a given key is in flight at any time
307
+ *
308
+ * Uses key to either retrieve in-flight promise, or if not
309
+ * create a new promise and call fetch.
310
+ *
311
+ * Note: The new promise is not actually tied to fetch at all,
312
+ * but is resolved when the expected 'recieve' action is processed.
313
+ * This ensures promises are resolved only once their data is processed
314
+ * by the reducer.
315
+ */
316
+ protected throttle(
317
+ key: string,
318
+ fetch: () => Promise<any>,
319
+ createdAt: number,
320
+ ) {
321
+ const lastReset = this.getLastReset();
322
+ // we're already fetching so reuse the promise
323
+ // fetches after reset do not count
324
+ if (key in this.fetched && this.fetchedAt[key] > lastReset) {
325
+ return this.fetched[key];
326
+ }
327
+
328
+ this.fetched[key] = new Promise((resolve, reject) => {
329
+ this.resolvers[key] = resolve;
330
+ this.rejectors[key] = reject;
331
+ });
332
+ this.fetchedAt[key] = createdAt;
333
+
334
+ // since our real promise is resolved via the wrapReducer(),
335
+ // we should just stop all errors here.
336
+ // TODO: decouple this from useFetcher() (that's what's dispatching the error the resolves in here)
337
+ RIC(
338
+ () => {
339
+ fetch().catch(() => null);
340
+ },
341
+ { timeout: 500 },
342
+ );
343
+
344
+ return this.fetched[key];
345
+ }
346
+ }
@@ -0,0 +1,190 @@
1
+ import type { EndpointInterface, Schema } from '@data-client/normalizr';
2
+
3
+ import ConnectionListener from './ConnectionListener.js';
4
+ import DefaultConnectionListener from './DefaultConnectionListener.js';
5
+ import { Subscription, SubscriptionInit } from './SubscriptionManager.js';
6
+ import createFetch from '../controller/createFetch.js';
7
+ import type { State, Dispatch } from '../types.js';
8
+
9
+ /**
10
+ * PollingSubscription keeps a given resource updated by
11
+ * dispatching a fetch at a rate equal to the minimum update
12
+ * interval requested.
13
+ *
14
+ * @see https://resthooks.io/docs/api/PollingSubscription
15
+ */
16
+ export default class PollingSubscription implements Subscription {
17
+ protected declare readonly schema: Schema | undefined;
18
+ protected declare readonly fetch: () => Promise<any>;
19
+ protected declare readonly key: string;
20
+ protected declare frequency: number;
21
+ protected frequencyHistogram: Map<number, number> = new Map();
22
+ protected declare dispatch: Dispatch<any>;
23
+ protected declare getState: () => State<unknown>;
24
+ protected declare intervalId?: ReturnType<typeof setInterval>;
25
+ protected declare lastIntervalId?: ReturnType<typeof setInterval>;
26
+ protected declare startId?: ReturnType<typeof setTimeout>;
27
+ private declare connectionListener: ConnectionListener;
28
+
29
+ constructor(
30
+ { key, schema, fetch, frequency, getState }: SubscriptionInit,
31
+ dispatch: Dispatch<any>,
32
+ connectionListener?: ConnectionListener,
33
+ ) {
34
+ if (frequency === undefined)
35
+ throw new Error('frequency needed for polling subscription');
36
+ this.schema = schema;
37
+ this.fetch = fetch;
38
+ this.frequency = frequency;
39
+ this.key = key;
40
+ this.frequencyHistogram.set(this.frequency, 1);
41
+ this.dispatch = dispatch;
42
+ this.getState = getState;
43
+ this.connectionListener =
44
+ connectionListener || new DefaultConnectionListener();
45
+
46
+ // Kickstart running since this is initialized after the online notif is sent
47
+ if (this.connectionListener.isOnline()) {
48
+ this.onlineListener();
49
+ } else {
50
+ this.offlineListener();
51
+ }
52
+ }
53
+
54
+ /** Subscribe to a frequency */
55
+ add(frequency?: number) {
56
+ if (frequency === undefined) return;
57
+ if (this.frequencyHistogram.has(frequency)) {
58
+ this.frequencyHistogram.set(
59
+ frequency,
60
+ (this.frequencyHistogram.get(frequency) as number) + 1,
61
+ );
62
+ } else {
63
+ this.frequencyHistogram.set(frequency, 1);
64
+
65
+ // new min so restart service
66
+ if (frequency < this.frequency) {
67
+ this.frequency = frequency;
68
+ this.run();
69
+ }
70
+ }
71
+ }
72
+
73
+ /** Unsubscribe from a frequency */
74
+ remove(frequency?: number) {
75
+ if (frequency === undefined) return false;
76
+ if (this.frequencyHistogram.has(frequency)) {
77
+ this.frequencyHistogram.set(
78
+ frequency,
79
+ (this.frequencyHistogram.get(frequency) as number) - 1,
80
+ );
81
+ if ((this.frequencyHistogram.get(frequency) as number) < 1) {
82
+ this.frequencyHistogram.delete(frequency);
83
+
84
+ // nothing subscribed to this anymore...it is invalid
85
+ if (this.frequencyHistogram.size === 0) {
86
+ this.cleanup();
87
+ return true;
88
+ }
89
+
90
+ // this was the min, so find the next size
91
+ if (frequency <= this.frequency) {
92
+ this.frequency = Math.min(...this.frequencyHistogram.keys());
93
+ this.run();
94
+ }
95
+ }
96
+ } /* istanbul ignore next */ else if (
97
+ process.env.NODE_ENV !== 'production'
98
+ ) {
99
+ console.error(
100
+ `Mismatched remove: ${frequency} is not subscribed for ${this.key}`,
101
+ );
102
+ }
103
+ return false;
104
+ }
105
+
106
+ /** Cleanup means clearing out background interval. */
107
+ cleanup() {
108
+ if (this.intervalId) {
109
+ clearInterval(this.intervalId);
110
+ delete this.intervalId;
111
+ }
112
+ if (this.lastIntervalId) {
113
+ clearInterval(this.lastIntervalId);
114
+ delete this.lastIntervalId;
115
+ }
116
+ if (this.startId) {
117
+ clearTimeout(this.startId);
118
+ delete this.startId;
119
+ }
120
+ this.connectionListener.removeOnlineListener(this.onlineListener);
121
+ this.connectionListener.removeOfflineListener(this.offlineListener);
122
+ }
123
+
124
+ /** Trigger request for latest resource */
125
+ protected update() {
126
+ const endpoint: EndpointInterface = () => this.fetch();
127
+ (endpoint as any).schema = this.schema;
128
+ endpoint.key = () => this.key;
129
+ (endpoint as any).dataExpiryLength = this.frequency / 2;
130
+ (endpoint as any).errorExpiryLength = this.frequency / 10;
131
+ endpoint.errorPolicy = () => 'soft' as const;
132
+ const action = createFetch(endpoint, { args: [] });
133
+ // stop any errors here from bubbling
134
+ (action.meta.promise as Promise<any>).catch(e => null);
135
+ this.dispatch(action);
136
+ }
137
+
138
+ /** What happens when browser goes offline */
139
+ protected offlineListener = () => {
140
+ // this clears existing listeners, so no need to clear offline listener
141
+ this.cleanup();
142
+ this.connectionListener.addOnlineListener(this.onlineListener);
143
+ };
144
+
145
+ /** What happens when browser comes online */
146
+ protected onlineListener = () => {
147
+ this.connectionListener.removeOnlineListener(this.onlineListener);
148
+ const now = Date.now();
149
+ this.startId = setTimeout(() => {
150
+ if (this.startId) {
151
+ delete this.startId;
152
+ this.update();
153
+ this.run();
154
+ } else if (process.env.NODE_ENV !== 'production') {
155
+ console.warn(
156
+ `Poll setTimeout for ${this.key} still running, but timeoutId deleted`,
157
+ );
158
+ }
159
+ }, Math.max(0, this.lastFetchTime() - now + this.frequency));
160
+ this.connectionListener.addOfflineListener(this.offlineListener);
161
+ };
162
+
163
+ /** Run polling process with current frequency
164
+ *
165
+ * Will clean up old poll interval on next run
166
+ */
167
+ protected run() {
168
+ if (this.startId) return;
169
+ if (this.intervalId) this.lastIntervalId = this.intervalId;
170
+ this.intervalId = setInterval(() => {
171
+ // since we don't know how long into the last poll it was before resetting
172
+ // we wait til the next fetch to clear old intervals
173
+ if (this.lastIntervalId) {
174
+ clearInterval(this.lastIntervalId);
175
+ delete this.lastIntervalId;
176
+ }
177
+ if (this.intervalId) this.update();
178
+ else if (process.env.NODE_ENV !== 'production') {
179
+ console.warn(
180
+ `Poll intervalId for ${this.key} still running, but intervalId deleted`,
181
+ );
182
+ }
183
+ }, this.frequency);
184
+ }
185
+
186
+ /** Last fetch time */
187
+ protected lastFetchTime() {
188
+ return this.getState().meta[this.key]?.date ?? 0;
189
+ }
190
+ }
@@ -0,0 +1,156 @@
1
+ import type { Schema } from '@data-client/normalizr';
2
+
3
+ import { SUBSCRIBE_TYPE, UNSUBSCRIBE_TYPE } from '../actionTypes.js';
4
+ import type {
5
+ Manager,
6
+ State,
7
+ MiddlewareAPI,
8
+ Middleware,
9
+ Dispatch,
10
+ UnsubscribeAction,
11
+ SubscribeAction,
12
+ } from '../types.js';
13
+
14
+ type Actions = UnsubscribeAction | SubscribeAction;
15
+
16
+ /** Properties sent to Subscription constructor */
17
+ export interface SubscriptionInit {
18
+ schema?: Schema | undefined;
19
+ fetch: () => Promise<any>;
20
+ key: string;
21
+ getState: () => State<unknown>;
22
+ frequency?: number | undefined;
23
+ }
24
+
25
+ /** Interface handling a single resource subscription */
26
+ export interface Subscription {
27
+ add(frequency?: number): void;
28
+ remove(frequency?: number): boolean;
29
+ cleanup(): void;
30
+ }
31
+
32
+ /** The static class that constructs Subscription */
33
+ export interface SubscriptionConstructable {
34
+ new (init: SubscriptionInit, dispatch: Dispatch<any>): Subscription;
35
+ }
36
+
37
+ /** Handles subscription actions -> fetch or receive actions
38
+ *
39
+ * Constructor takes a SubscriptionConstructable class to control how
40
+ * subscriptions are handled. (e.g., polling, websockets)
41
+ *
42
+ * @see https://resthooks.io/docs/api/SubscriptionManager
43
+ */
44
+ export default class SubscriptionManager<S extends SubscriptionConstructable>
45
+ implements Manager
46
+ {
47
+ protected subscriptions: {
48
+ [key: string]: InstanceType<S>;
49
+ } = {};
50
+
51
+ protected declare readonly Subscription: S;
52
+ protected declare middleware: Middleware;
53
+
54
+ constructor(Subscription: S) {
55
+ this.Subscription = Subscription;
56
+
57
+ this.middleware = <C extends MiddlewareAPI>({ dispatch, getState }: C) => {
58
+ return (next: C['dispatch']): C['dispatch'] =>
59
+ action => {
60
+ switch (action.type) {
61
+ case SUBSCRIBE_TYPE:
62
+ try {
63
+ this.handleSubscribe(action, dispatch, getState);
64
+ } catch (e) {
65
+ console.error(e);
66
+ }
67
+ return Promise.resolve();
68
+ case UNSUBSCRIBE_TYPE:
69
+ this.handleUnsubscribe(action, dispatch);
70
+ return Promise.resolve();
71
+ default:
72
+ return next(action);
73
+ }
74
+ };
75
+ };
76
+ }
77
+
78
+ /** Ensures all subscriptions are cleaned up. */
79
+ cleanup() {
80
+ for (const key in this.subscriptions) {
81
+ this.subscriptions[key].cleanup();
82
+ }
83
+ }
84
+
85
+ /** Called when middleware intercepts 'rest-hooks/subscribe' action.
86
+ *
87
+ */
88
+ protected handleSubscribe(
89
+ action: SubscribeAction,
90
+ dispatch: (action: any) => Promise<void>,
91
+ getState: () => State<unknown>,
92
+ ) {
93
+ let options: SubscriptionInit;
94
+ if (action.endpoint) {
95
+ const { endpoint } = action;
96
+ const { args } = action.meta;
97
+ options = {
98
+ schema: endpoint.schema,
99
+ fetch: () => endpoint(...args),
100
+ frequency: endpoint.pollFrequency,
101
+ key: endpoint.key(...args),
102
+ getState,
103
+ };
104
+ } else {
105
+ options = {
106
+ key: action.meta.key,
107
+ frequency: action.meta.options?.pollFrequency,
108
+ schema: action.meta.schema,
109
+ fetch: action.meta.fetch,
110
+ getState,
111
+ };
112
+ }
113
+
114
+ if (options.key in this.subscriptions) {
115
+ this.subscriptions[options.key].add(options.frequency);
116
+ } else {
117
+ this.subscriptions[options.key] = new this.Subscription(
118
+ options,
119
+ dispatch,
120
+ ) as InstanceType<S>;
121
+ }
122
+ }
123
+
124
+ /** Called when middleware intercepts 'rest-hooks/unsubscribe' action.
125
+ *
126
+ */
127
+ protected handleUnsubscribe(
128
+ action: UnsubscribeAction,
129
+ dispatch: (action: any) => Promise<void>,
130
+ ) {
131
+ const key = action.meta.key;
132
+ const frequency = action.meta.options?.pollFrequency;
133
+
134
+ /* istanbul ignore else */
135
+ if (key in this.subscriptions) {
136
+ const empty = this.subscriptions[key].remove(frequency);
137
+ if (empty) {
138
+ delete this.subscriptions[key];
139
+ }
140
+ } else if (process.env.NODE_ENV !== 'production') {
141
+ console.error(`Mismatched unsubscribe: ${key} is not subscribed`);
142
+ }
143
+ }
144
+
145
+ /** Attaches Manager to store
146
+ *
147
+ * Intercepts 'rest-hooks/subscribe'/'rest-hooks/unsubscribe' to register resources that
148
+ * need to be kept up to date.
149
+ *
150
+ * Will possibly dispatch 'rest-hooks/fetch' or 'rest-hooks/receive' to keep resources fresh
151
+ *
152
+ */
153
+ getMiddleware() {
154
+ return this.middleware;
155
+ }
156
+ }