@ethlete/query 5.15.1 → 5.16.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 (184) hide show
  1. package/.eslintrc.json +40 -0
  2. package/CHANGELOG.md +8 -0
  3. package/jest.config.ts +23 -0
  4. package/ng-package.json +8 -0
  5. package/package.json +6 -23
  6. package/project.json +37 -0
  7. package/{index.d.ts → src/index.ts} +2 -0
  8. package/src/lib/auth/auth-provider.types.ts +160 -0
  9. package/src/lib/auth/auth-provider.utils.ts +39 -0
  10. package/src/lib/auth/basic-auth-provider.ts +17 -0
  11. package/src/lib/auth/bearer-auth-provider.ts +243 -0
  12. package/src/lib/auth/custom-header-auth-provider.ts +13 -0
  13. package/{lib/class-validator/class-validator.types.d.ts → src/lib/class-validator/class-validator.types.ts} +4 -3
  14. package/src/lib/class-validator/class-validator.utils.ts +5 -0
  15. package/src/lib/devtools/components/query-devtools/query-devtools.component.html +402 -0
  16. package/src/lib/devtools/components/query-devtools/query-devtools.component.scss +360 -0
  17. package/src/lib/devtools/components/query-devtools/query-devtools.component.ts +427 -0
  18. package/src/lib/devtools/pipes/query-short-name.pipe.ts +24 -0
  19. package/src/lib/devtools/utils/query-client.utils.ts +25 -0
  20. package/src/lib/directives/infinity-query-trigger.directive.ts +58 -0
  21. package/src/lib/directives/infinity-query.directive.ts +208 -0
  22. package/src/lib/directives/query.directive.ts +201 -0
  23. package/src/lib/entity/entity-store.ts +180 -0
  24. package/src/lib/entity/entity.types.ts +34 -0
  25. package/src/lib/entity/entity.utils.ts +51 -0
  26. package/src/lib/gql/gql.ts +22 -0
  27. package/src/lib/infinite-query/infinity-query.ts +245 -0
  28. package/src/lib/infinite-query/infinity-query.types.ts +149 -0
  29. package/src/lib/infinite-query/infinity-query.utils.ts +14 -0
  30. package/src/lib/logger/logger.ts +30 -0
  31. package/src/lib/query/query.ts +631 -0
  32. package/src/lib/query/query.types.ts +532 -0
  33. package/src/lib/query/query.utils.spec.ts +92 -0
  34. package/src/lib/query/query.utils.ts +450 -0
  35. package/src/lib/query-client/query-client.spec.ts +77 -0
  36. package/src/lib/query-client/query-client.ts +195 -0
  37. package/src/lib/query-client/query-client.types.ts +101 -0
  38. package/src/lib/query-client/query-client.utils.spec.ts +61 -0
  39. package/src/lib/query-client/query-client.utils.ts +28 -0
  40. package/src/lib/query-creator/query-creator.ts +103 -0
  41. package/src/lib/query-creator/query-creator.types.ts +74 -0
  42. package/src/lib/query-form/query-form.ts +526 -0
  43. package/src/lib/query-form/query-form.types.ts +169 -0
  44. package/src/lib/query-form/query-form.utils.ts +116 -0
  45. package/src/lib/query-store/query-store.ts +192 -0
  46. package/src/lib/request/request.ts +253 -0
  47. package/src/lib/request/request.types.ts +164 -0
  48. package/src/lib/request/request.util.spec.ts +177 -0
  49. package/src/lib/request/request.util.ts +399 -0
  50. package/src/lib/stories/angular-example-page.mdx +85 -0
  51. package/src/lib/stories/authentication-page.mdx +35 -0
  52. package/src/lib/stories/changelog-page.mdx +9 -0
  53. package/src/lib/stories/client-page.mdx +100 -0
  54. package/src/lib/stories/queries-page.mdx +117 -0
  55. package/src/lib/symfony/symfony.types.ts +21 -0
  56. package/src/lib/symfony/symfony.utils.ts +43 -0
  57. package/src/lib/utils/data.utils.ts +280 -0
  58. package/src/lib/utils/deep-freeze.util.ts +21 -0
  59. package/{lib/utils/def.util.d.ts → src/lib/utils/def.util.ts} +1 -1
  60. package/src/test-setup.ts +1 -0
  61. package/tsconfig.json +29 -0
  62. package/tsconfig.lib.json +12 -0
  63. package/tsconfig.lib.prod.json +10 -0
  64. package/tsconfig.spec.json +11 -0
  65. package/esm2022/ethlete-query.mjs +0 -5
  66. package/esm2022/index.mjs +0 -20
  67. package/esm2022/lib/auth/auth-provider.types.mjs +0 -2
  68. package/esm2022/lib/auth/auth-provider.utils.mjs +0 -25
  69. package/esm2022/lib/auth/basic-auth-provider.mjs +0 -15
  70. package/esm2022/lib/auth/bearer-auth-provider.mjs +0 -168
  71. package/esm2022/lib/auth/custom-header-auth-provider.mjs +0 -12
  72. package/esm2022/lib/auth/index.mjs +0 -6
  73. package/esm2022/lib/class-validator/class-validator.types.mjs +0 -2
  74. package/esm2022/lib/class-validator/class-validator.utils.mjs +0 -4
  75. package/esm2022/lib/class-validator/index.mjs +0 -3
  76. package/esm2022/lib/devtools/components/query-devtools/index.mjs +0 -2
  77. package/esm2022/lib/devtools/components/query-devtools/query-devtools.component.mjs +0 -297
  78. package/esm2022/lib/devtools/pipes/index.mjs +0 -2
  79. package/esm2022/lib/devtools/pipes/query-short-name.pipe.mjs +0 -24
  80. package/esm2022/lib/devtools/utils/index.mjs +0 -2
  81. package/esm2022/lib/devtools/utils/query-client.utils.mjs +0 -13
  82. package/esm2022/lib/directives/index.mjs +0 -4
  83. package/esm2022/lib/directives/infinity-query-trigger.directive.mjs +0 -56
  84. package/esm2022/lib/directives/infinity-query.directive.mjs +0 -147
  85. package/esm2022/lib/directives/query.directive.mjs +0 -123
  86. package/esm2022/lib/entity/entity-store.mjs +0 -127
  87. package/esm2022/lib/entity/entity.types.mjs +0 -2
  88. package/esm2022/lib/entity/entity.utils.mjs +0 -29
  89. package/esm2022/lib/entity/index.mjs +0 -4
  90. package/esm2022/lib/gql/gql.mjs +0 -23
  91. package/esm2022/lib/gql/index.mjs +0 -2
  92. package/esm2022/lib/infinite-query/index.mjs +0 -4
  93. package/esm2022/lib/infinite-query/infinity-query.mjs +0 -171
  94. package/esm2022/lib/infinite-query/infinity-query.types.mjs +0 -2
  95. package/esm2022/lib/infinite-query/infinity-query.utils.mjs +0 -3
  96. package/esm2022/lib/logger/index.mjs +0 -2
  97. package/esm2022/lib/logger/logger.mjs +0 -16
  98. package/esm2022/lib/query/index.mjs +0 -4
  99. package/esm2022/lib/query/query.mjs +0 -428
  100. package/esm2022/lib/query/query.types.mjs +0 -2
  101. package/esm2022/lib/query/query.utils.mjs +0 -253
  102. package/esm2022/lib/query-client/index.mjs +0 -4
  103. package/esm2022/lib/query-client/query-client.mjs +0 -80
  104. package/esm2022/lib/query-client/query-client.types.mjs +0 -2
  105. package/esm2022/lib/query-client/query-client.utils.mjs +0 -20
  106. package/esm2022/lib/query-creator/index.mjs +0 -3
  107. package/esm2022/lib/query-creator/query-creator.mjs +0 -55
  108. package/esm2022/lib/query-creator/query-creator.types.mjs +0 -2
  109. package/esm2022/lib/query-form/index.mjs +0 -4
  110. package/esm2022/lib/query-form/query-form.mjs +0 -368
  111. package/esm2022/lib/query-form/query-form.types.mjs +0 -2
  112. package/esm2022/lib/query-form/query-form.utils.mjs +0 -97
  113. package/esm2022/lib/query-store/index.mjs +0 -2
  114. package/esm2022/lib/query-store/query-store.mjs +0 -144
  115. package/esm2022/lib/request/index.mjs +0 -4
  116. package/esm2022/lib/request/request.mjs +0 -196
  117. package/esm2022/lib/request/request.types.mjs +0 -2
  118. package/esm2022/lib/request/request.util.mjs +0 -252
  119. package/esm2022/lib/symfony/index.mjs +0 -3
  120. package/esm2022/lib/symfony/symfony.types.mjs +0 -2
  121. package/esm2022/lib/symfony/symfony.utils.mjs +0 -31
  122. package/esm2022/lib/utils/data.utils.mjs +0 -138
  123. package/esm2022/lib/utils/deep-freeze.util.mjs +0 -17
  124. package/esm2022/lib/utils/def.util.mjs +0 -8
  125. package/esm2022/lib/utils/index.mjs +0 -4
  126. package/fesm2022/ethlete-query.mjs +0 -3276
  127. package/fesm2022/ethlete-query.mjs.map +0 -1
  128. package/lib/auth/auth-provider.types.d.ts +0 -133
  129. package/lib/auth/auth-provider.utils.d.ts +0 -9
  130. package/lib/auth/basic-auth-provider.d.ts +0 -10
  131. package/lib/auth/bearer-auth-provider.d.ts +0 -23
  132. package/lib/auth/custom-header-auth-provider.d.ts +0 -9
  133. package/lib/class-validator/class-validator.utils.d.ts +0 -2
  134. package/lib/devtools/components/query-devtools/query-devtools.component.d.ts +0 -151
  135. package/lib/devtools/pipes/query-short-name.pipe.d.ts +0 -8
  136. package/lib/devtools/utils/query-client.utils.d.ts +0 -12
  137. package/lib/directives/infinity-query-trigger.directive.d.ts +0 -15
  138. package/lib/directives/infinity-query.directive.d.ts +0 -49
  139. package/lib/directives/query.directive.d.ts +0 -63
  140. package/lib/entity/entity-store.d.ts +0 -25
  141. package/lib/entity/entity.types.d.ts +0 -25
  142. package/lib/entity/entity.utils.d.ts +0 -12
  143. package/lib/gql/gql.d.ts +0 -6
  144. package/lib/infinite-query/infinity-query.d.ts +0 -29
  145. package/lib/infinite-query/infinity-query.types.d.ts +0 -104
  146. package/lib/infinite-query/infinity-query.utils.d.ts +0 -4
  147. package/lib/logger/logger.d.ts +0 -12
  148. package/lib/query/query.d.ts +0 -60
  149. package/lib/query/query.types.d.ts +0 -388
  150. package/lib/query/query.utils.d.ts +0 -91
  151. package/lib/query-client/query-client.d.ts +0 -29
  152. package/lib/query-client/query-client.types.d.ts +0 -82
  153. package/lib/query-client/query-client.utils.d.ts +0 -4
  154. package/lib/query-creator/query-creator.d.ts +0 -20
  155. package/lib/query-creator/query-creator.types.d.ts +0 -13
  156. package/lib/query-form/query-form.d.ts +0 -89
  157. package/lib/query-form/query-form.types.d.ts +0 -138
  158. package/lib/query-form/query-form.utils.d.ts +0 -13
  159. package/lib/query-store/query-store.d.ts +0 -31
  160. package/lib/request/request.d.ts +0 -3
  161. package/lib/request/request.types.d.ts +0 -126
  162. package/lib/request/request.util.d.ts +0 -92
  163. package/lib/symfony/symfony.types.d.ts +0 -19
  164. package/lib/symfony/symfony.utils.d.ts +0 -6
  165. package/lib/utils/data.utils.d.ts +0 -59
  166. package/lib/utils/deep-freeze.util.d.ts +0 -1
  167. /package/{lib/auth/index.d.ts → src/lib/auth/index.ts} +0 -0
  168. /package/{lib/class-validator/index.d.ts → src/lib/class-validator/index.ts} +0 -0
  169. /package/{lib/devtools/components/query-devtools/index.d.ts → src/lib/devtools/components/query-devtools/index.ts} +0 -0
  170. /package/{lib/devtools/pipes/index.d.ts → src/lib/devtools/pipes/index.ts} +0 -0
  171. /package/{lib/devtools/utils/index.d.ts → src/lib/devtools/utils/index.ts} +0 -0
  172. /package/{lib/directives/index.d.ts → src/lib/directives/index.ts} +0 -0
  173. /package/{lib/entity/index.d.ts → src/lib/entity/index.ts} +0 -0
  174. /package/{lib/gql/index.d.ts → src/lib/gql/index.ts} +0 -0
  175. /package/{lib/infinite-query/index.d.ts → src/lib/infinite-query/index.ts} +0 -0
  176. /package/{lib/logger/index.d.ts → src/lib/logger/index.ts} +0 -0
  177. /package/{lib/query/index.d.ts → src/lib/query/index.ts} +0 -0
  178. /package/{lib/query-client/index.d.ts → src/lib/query-client/index.ts} +0 -0
  179. /package/{lib/query-creator/index.d.ts → src/lib/query-creator/index.ts} +0 -0
  180. /package/{lib/query-form/index.d.ts → src/lib/query-form/index.ts} +0 -0
  181. /package/{lib/query-store/index.d.ts → src/lib/query-store/index.ts} +0 -0
  182. /package/{lib/request/index.d.ts → src/lib/request/index.ts} +0 -0
  183. /package/{lib/symfony/index.d.ts → src/lib/symfony/index.ts} +0 -0
  184. /package/{lib/utils/index.d.ts → src/lib/utils/index.ts} +0 -0
@@ -0,0 +1,116 @@
1
+ export const transformToString = (value: unknown) => {
2
+ if (typeof value === 'string') {
3
+ return value;
4
+ }
5
+
6
+ return null;
7
+ };
8
+
9
+ export const transformToStringArray = (value: unknown) => {
10
+ if (Array.isArray(value)) {
11
+ return value.map(transformToString).filter(Boolean) as string[];
12
+ } else if (typeof value === 'string') {
13
+ return [transformToString(value)] as string[];
14
+ }
15
+
16
+ return null;
17
+ };
18
+
19
+ export const transformToNumber = (value: unknown) => {
20
+ if (typeof value === 'number') {
21
+ return value;
22
+ }
23
+ if (typeof value === 'string') {
24
+ return Number(value);
25
+ }
26
+ return null;
27
+ };
28
+
29
+ export const transformToNumberArray = (value: unknown) => {
30
+ if (Array.isArray(value)) {
31
+ return value.map(transformToNumber).filter((item) => item !== null) as number[];
32
+ } else if (typeof value === 'number') {
33
+ return [value] as number[];
34
+ }
35
+
36
+ return null;
37
+ };
38
+
39
+ export const transformToBoolean = (value: unknown) => {
40
+ if (typeof value === 'boolean') {
41
+ return value;
42
+ }
43
+ if (typeof value === 'string') {
44
+ return value === 'true' || value === '1';
45
+ }
46
+ return null;
47
+ };
48
+
49
+ export const transformToBooleanArray = (value: unknown) => {
50
+ if (Array.isArray(value)) {
51
+ return value.map(transformToBoolean).filter((item) => item !== null) as boolean[];
52
+ } else if (typeof value === 'string' || typeof value === 'boolean') {
53
+ return [transformToBoolean(value)] as boolean[];
54
+ }
55
+
56
+ return null;
57
+ };
58
+
59
+ export const transformToDate = (value: unknown) => {
60
+ if (value instanceof Date) {
61
+ return value;
62
+ }
63
+ if (typeof value === 'string') {
64
+ const date = new Date(value);
65
+
66
+ if (isNaN(date.getTime())) {
67
+ return null;
68
+ }
69
+
70
+ return date;
71
+ }
72
+ return null;
73
+ };
74
+
75
+ export const transformToDateArray = (value: unknown) => {
76
+ if (Array.isArray(value)) {
77
+ return value.map(transformToDate).filter((item) => item !== null) as Date[];
78
+ } else if (typeof value === 'string' || value instanceof Date) {
79
+ return [transformToDate(value)] as Date[];
80
+ }
81
+
82
+ return null;
83
+ };
84
+
85
+ export const transformToSort = (value: unknown) => {
86
+ if (typeof value === 'string') {
87
+ const [active, direction] = value.split(':');
88
+
89
+ if (!active) {
90
+ return null;
91
+ }
92
+
93
+ return {
94
+ active,
95
+ direction: direction as 'asc' | 'desc',
96
+ };
97
+ }
98
+
99
+ return null;
100
+ };
101
+
102
+ export const transformToSortQueryParam = (value: unknown) => {
103
+ if (typeof value === 'string') {
104
+ return value;
105
+ }
106
+ if (value && typeof value === 'object' && 'active' in value && 'direction' in value) {
107
+ const valAsSort = value as {
108
+ active: string;
109
+ direction: 'asc' | 'desc';
110
+ };
111
+
112
+ return valAsSort.direction ? `${valAsSort.active}:${valAsSort.direction}` : null;
113
+ }
114
+
115
+ return null;
116
+ };
@@ -0,0 +1,192 @@
1
+ import { Subject, fromEvent, take, takeUntil, timer } from 'rxjs';
2
+ import { AnyQuery } from '../query';
3
+
4
+ export class QueryStore {
5
+ /**
6
+ * @internal
7
+ */
8
+ readonly _store = new Map<string, AnyQuery>();
9
+
10
+ private _garbageCollector: number | null = null;
11
+ private _isInLowResourceMode = false;
12
+ private _lastBlurTimestamp = Date.now();
13
+
14
+ private _storeChange$ = new Subject<string>();
15
+ private _queryCreated$ = new Subject<AnyQuery>();
16
+
17
+ readonly storeChange$ = this._storeChange$.asObservable();
18
+ readonly queryCreated$ = this._queryCreated$.asObservable();
19
+
20
+ constructor(
21
+ private _config?: {
22
+ enableChangeLogging?: boolean;
23
+ enableGarbageCollectorLogging?: boolean;
24
+ autoRefreshQueriesOnWindowFocus?: boolean;
25
+ enableSmartPolling?: boolean;
26
+ },
27
+ ) {
28
+ this._initSmartQueryHandling();
29
+ }
30
+
31
+ add(id: string, query: AnyQuery) {
32
+ this._store.set(id, query);
33
+
34
+ this._initGarbageCollector();
35
+
36
+ this._logState(id, query, 'SET');
37
+
38
+ this._storeChange$.next(id);
39
+ }
40
+
41
+ get<T extends AnyQuery>(id: string): T | null {
42
+ return (this._store.get(id) as T) ?? null;
43
+ }
44
+
45
+ remove(id: string) {
46
+ this._store.delete(id);
47
+
48
+ this._logState(id, null, 'REMOVE');
49
+
50
+ this._storeChange$.next(id);
51
+ }
52
+
53
+ forEach(callback: (value: AnyQuery, key: string) => void) {
54
+ for (const [key, query] of this._store) {
55
+ callback(query, key);
56
+ }
57
+ }
58
+
59
+ refreshQueriesInUse(config?: { ignoreCacheValidity?: boolean; purgeUnused?: boolean }) {
60
+ const { ignoreCacheValidity, purgeUnused } = config ?? {};
61
+
62
+ for (const [key, query] of this._store) {
63
+ if (
64
+ query.isInUse &&
65
+ (query.isExpired || ignoreCacheValidity) &&
66
+ query.autoRefreshOnConfig.queryClientDefaultHeadersChange
67
+ ) {
68
+ query.execute({ skipCache: true, _triggeredVia: 'auto' });
69
+ } else if (purgeUnused && !query.isInUse) {
70
+ this.remove(key);
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * @internal
77
+ */
78
+ _dispatchQueryCreated(query: AnyQuery) {
79
+ this._queryCreated$.next(query);
80
+ }
81
+
82
+ private _initSmartQueryHandling() {
83
+ const windowBlur$ = fromEvent<Event>(window, 'blur');
84
+ const windowFocus$ = fromEvent<Event>(window, 'focus');
85
+
86
+ windowBlur$.subscribe(() => {
87
+ timer(5000)
88
+ .pipe(takeUntil(windowFocus$), take(1))
89
+ .subscribe(() => {
90
+ this._lastBlurTimestamp = Date.now();
91
+ this._isInLowResourceMode = true;
92
+ this._stopGarbageCollector();
93
+
94
+ if (this._config?.enableSmartPolling) {
95
+ this.forEach((query) => {
96
+ if (!query.isPolling || !query._enableSmartPolling) {
97
+ return;
98
+ }
99
+
100
+ query.pausePolling();
101
+ });
102
+ }
103
+ });
104
+ });
105
+
106
+ windowFocus$.subscribe(() => {
107
+ if (!this._isInLowResourceMode) {
108
+ return;
109
+ }
110
+
111
+ this._isInLowResourceMode = false;
112
+
113
+ if (this._config?.enableSmartPolling || this._config?.autoRefreshQueriesOnWindowFocus) {
114
+ this.forEach((query) => {
115
+ if (this._config?.enableSmartPolling && query._isPollingPaused) {
116
+ query.resumePolling();
117
+ }
118
+
119
+ if (Date.now() - this._lastBlurTimestamp > 15000) {
120
+ if (
121
+ this._config?.autoRefreshQueriesOnWindowFocus &&
122
+ query.isExpired &&
123
+ query.isInUse &&
124
+ query.autoRefreshOnConfig.windowFocus
125
+ ) {
126
+ query.execute({ skipCache: true, _triggeredVia: 'auto' });
127
+ }
128
+ }
129
+ });
130
+ }
131
+
132
+ this._initGarbageCollector();
133
+ });
134
+ }
135
+
136
+ private _logState(key: string | null, item: AnyQuery | null, operation: string) {
137
+ if (!this._config?.enableChangeLogging) return;
138
+
139
+ const stateAsJson: Record<string, AnyQuery> = {};
140
+
141
+ this._store.forEach((value, key) => {
142
+ stateAsJson[key] = value;
143
+ });
144
+
145
+ console.log(`%c[${operation}] ${key}`, 'font-weight: bold');
146
+
147
+ console.log({ operation, key, item });
148
+ console.log(stateAsJson);
149
+ }
150
+
151
+ private _initGarbageCollector() {
152
+ if (this._garbageCollector !== null) {
153
+ return;
154
+ }
155
+
156
+ this._logGarbageCollector('Start');
157
+
158
+ this._garbageCollector = window.setInterval(() => {
159
+ this._runGarbageCollector();
160
+ }, 15000);
161
+ }
162
+
163
+ private _stopGarbageCollector() {
164
+ if (this._garbageCollector !== null) {
165
+ window.clearInterval(this._garbageCollector);
166
+ this._garbageCollector = null;
167
+ this._logGarbageCollector('Stop');
168
+ }
169
+ }
170
+
171
+ private _runGarbageCollector() {
172
+ this._logGarbageCollector('Collecting...');
173
+
174
+ this._store.forEach((item, key) => {
175
+ if (item.isExpired && !item.isInUse) {
176
+ this.remove(key);
177
+ }
178
+ });
179
+
180
+ this._logGarbageCollector('Collection done');
181
+
182
+ if (!this._store.size) {
183
+ this._stopGarbageCollector();
184
+ }
185
+ }
186
+
187
+ private _logGarbageCollector(action: string) {
188
+ if (!this._config?.enableGarbageCollectorLogging) return;
189
+
190
+ console.log(`%cGC: ${action}`, 'color: yellow; font-weight: bold');
191
+ }
192
+ }
@@ -0,0 +1,253 @@
1
+ import { Observable, Observer } from 'rxjs';
2
+ import { PartialXhrState, RequestConfig, RequestError, RequestEvent } from './request.types';
3
+ import {
4
+ HttpStatusCode,
5
+ buildTimestampFromSeconds,
6
+ detectContentTypeHeader,
7
+ extractExpiresInSeconds,
8
+ forEachHeader,
9
+ getResponseUrl,
10
+ hasHeader,
11
+ parseAllXhrResponseHeaders,
12
+ serializeBody,
13
+ shouldRetryRequest,
14
+ } from './request.util';
15
+
16
+ const XSSI_PREFIX = /^\)\]\}',?\n/;
17
+
18
+ export const request = <Response = unknown>(config: RequestConfig): Observable<RequestEvent<Response>> => {
19
+ const headers = config.headers || {};
20
+ const responseType = config.responseType || 'json';
21
+ const body = config.body || null;
22
+ const url = config.urlWithParams.split('?')[0] || '';
23
+ const retryFn = config.retryFn || shouldRetryRequest;
24
+ let currentRetryCount = 0;
25
+ let retryTimeout: number | null = null;
26
+
27
+ return new Observable((observer: Observer<RequestEvent<Response>>) => {
28
+ const xhr = new XMLHttpRequest();
29
+ const reqBody = serializeBody(body);
30
+
31
+ const setupXhr = () => {
32
+ xhr.open(config.method, config.urlWithParams);
33
+ if (config.withCredentials) {
34
+ xhr.withCredentials = true;
35
+ }
36
+
37
+ forEachHeader(headers, (name, value) => xhr.setRequestHeader(name, value));
38
+
39
+ if (!hasHeader(headers, 'Accept')) {
40
+ xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
41
+ }
42
+
43
+ if (!hasHeader(headers, 'Content-Type')) {
44
+ const detectedType = detectContentTypeHeader(body);
45
+
46
+ if (detectedType) {
47
+ xhr.setRequestHeader('Content-Type', detectedType);
48
+ }
49
+ }
50
+
51
+ xhr.responseType = responseType !== 'json' ? responseType : 'text';
52
+ };
53
+
54
+ setupXhr();
55
+
56
+ const partialFromXhr = (): PartialXhrState => {
57
+ const statusText = xhr.statusText || 'OK';
58
+ const headers = parseAllXhrResponseHeaders(xhr);
59
+ const _url = getResponseUrl(xhr) || url;
60
+
61
+ return { headers, status: xhr.status, statusText, url: _url };
62
+ };
63
+
64
+ const handleRetry = (error: RequestError) => {
65
+ const newRetryCount = currentRetryCount + 1;
66
+ const { headers } = partialFromXhr();
67
+ const { retry, delay } = retryFn({ currentRetryCount: newRetryCount, headers, error });
68
+
69
+ if (!retry) {
70
+ observer.next({ type: 'failure', headers, error });
71
+ observer.complete();
72
+ return;
73
+ }
74
+
75
+ const _delay = delay ?? 1000;
76
+ observer.next({ type: 'delay-retry', headers, retryNumber: newRetryCount, retryDelay: _delay });
77
+
78
+ retryTimeout = window.setTimeout(() => {
79
+ setupXhr();
80
+ xhr.send(reqBody);
81
+ currentRetryCount = newRetryCount;
82
+ observer.next({ type: 'start', headers, isRetry: true, retryNumber: currentRetryCount, retryDelay: _delay });
83
+ }, _delay);
84
+ };
85
+
86
+ const onLoad = async () => {
87
+ const { headers, statusText, url } = partialFromXhr();
88
+
89
+ let { status } = partialFromXhr();
90
+
91
+ let body: unknown | null = null;
92
+
93
+ if (status !== HttpStatusCode.NoContent) {
94
+ body = typeof xhr.response === 'undefined' ? xhr.responseText : xhr.response;
95
+ }
96
+
97
+ if (status === 0) {
98
+ status = body ? HttpStatusCode.Ok : 0;
99
+ }
100
+
101
+ let ok = status >= 200 && status < 300;
102
+
103
+ if (responseType === 'json' && typeof body === 'string') {
104
+ const originalBody = body;
105
+ body = body.replace(XSSI_PREFIX, '');
106
+ try {
107
+ body = body !== '' ? JSON.parse(body as string) : null;
108
+ } catch (error) {
109
+ body = originalBody;
110
+
111
+ if (ok) {
112
+ ok = false;
113
+ body = { error, text: body };
114
+ }
115
+ }
116
+ } else if (!ok && body instanceof Blob) {
117
+ try {
118
+ const text = await body.text();
119
+
120
+ if (body.type === 'application/json') {
121
+ body = JSON.parse(text);
122
+ } else {
123
+ body = text;
124
+ }
125
+ } catch (error) {
126
+ // Ignore the error.
127
+ }
128
+ }
129
+
130
+ if (ok) {
131
+ const successEvent: RequestEvent<Response> = {
132
+ type: 'success',
133
+ headers,
134
+ response: body as Response,
135
+ };
136
+
137
+ const expiresInSeconds = config.cacheAdapter?.(headers) ?? extractExpiresInSeconds(headers);
138
+ const expiresInTimestamp = buildTimestampFromSeconds(expiresInSeconds);
139
+
140
+ if (expiresInTimestamp) {
141
+ successEvent.expiresInTimestamp = expiresInTimestamp;
142
+ }
143
+
144
+ observer.next(successEvent);
145
+
146
+ observer.complete();
147
+ } else {
148
+ handleRetry({
149
+ url,
150
+ detail: body,
151
+ status,
152
+ statusText,
153
+ });
154
+ }
155
+ };
156
+
157
+ const onError = (error: Event) => {
158
+ const { url } = partialFromXhr();
159
+
160
+ handleRetry({
161
+ url,
162
+ detail: error,
163
+ status: xhr.status || 0,
164
+ statusText: xhr.statusText || 'Unknown Error',
165
+ });
166
+ };
167
+
168
+ const onDownProgress = (event: ProgressEvent) => {
169
+ const { headers } = partialFromXhr();
170
+
171
+ const progress: RequestEvent = {
172
+ type: 'download-progress',
173
+ headers,
174
+ progress: {
175
+ current: event.loaded,
176
+ },
177
+ };
178
+
179
+ if (event.lengthComputable) {
180
+ const progressPercent = (event.loaded / event.total) * 100;
181
+ progress.progress.percentage = Math.round(progressPercent * 100) / 100;
182
+ progress.progress.total = event.total;
183
+ }
184
+
185
+ if (responseType === 'text' && !!xhr.responseText) {
186
+ progress.partialText = xhr.responseText;
187
+ }
188
+
189
+ observer.next(progress);
190
+ };
191
+
192
+ const onUpProgress = (event: ProgressEvent) => {
193
+ const { headers } = partialFromXhr();
194
+
195
+ const progress: RequestEvent = {
196
+ type: 'upload-progress',
197
+ headers,
198
+ progress: {
199
+ current: event.loaded,
200
+ },
201
+ };
202
+
203
+ if (event.lengthComputable) {
204
+ const progressPercent = (event.loaded / event.total) * 100;
205
+ progress.progress.percentage = Math.round(progressPercent * 100) / 100;
206
+ progress.progress.total = event.total;
207
+ }
208
+
209
+ observer.next(progress);
210
+ };
211
+
212
+ xhr.addEventListener('load', onLoad);
213
+ xhr.addEventListener('error', onError);
214
+ xhr.addEventListener('timeout', onError);
215
+ xhr.addEventListener('abort', onError);
216
+
217
+ if (config.reportProgress) {
218
+ xhr.addEventListener('progress', onDownProgress);
219
+
220
+ if (reqBody !== null && xhr.upload) {
221
+ xhr.upload.addEventListener('progress', onUpProgress);
222
+ }
223
+ }
224
+
225
+ xhr.send(reqBody);
226
+ observer.next({ type: 'start', headers });
227
+
228
+ return () => {
229
+ xhr.removeEventListener('error', onError);
230
+ xhr.removeEventListener('abort', onError);
231
+ xhr.removeEventListener('load', onLoad);
232
+ xhr.removeEventListener('timeout', onError);
233
+
234
+ if (retryTimeout) {
235
+ window.clearTimeout(retryTimeout);
236
+ }
237
+
238
+ if (config.reportProgress) {
239
+ xhr.removeEventListener('progress', onDownProgress);
240
+
241
+ if (reqBody !== null && xhr.upload) {
242
+ xhr.upload.removeEventListener('progress', onUpProgress);
243
+ }
244
+ }
245
+
246
+ if (xhr.readyState !== xhr.DONE) {
247
+ xhr.abort();
248
+ }
249
+
250
+ observer.next({ type: 'cancel', headers });
251
+ };
252
+ });
253
+ };
@@ -0,0 +1,164 @@
1
+ import { HttpStatusCode } from './request.util';
2
+
3
+ export type Method =
4
+ | 'GET'
5
+ | 'POST'
6
+ | 'PUT'
7
+ | 'DELETE'
8
+ | 'PATCH'
9
+ | 'HEAD'
10
+ | 'OPTIONS'
11
+ | 'TRACE'
12
+ | 'CONNECT'
13
+ | 'PURGE'
14
+ | 'LINK'
15
+ | 'UNLINK'
16
+ | 'GQL_QUERY'
17
+ | 'GQL_MUTATE'
18
+ | 'CUSTOM';
19
+
20
+ export interface RequestError<Detail = unknown> {
21
+ url: string;
22
+ status: HttpStatusCode;
23
+ statusText: string;
24
+ detail: Detail;
25
+ }
26
+
27
+ export type RequestHeaders = Record<string, string>;
28
+ export type QueryParams = object;
29
+ export type PathParams = Record<string, string | number>;
30
+
31
+ export type CacheAdapterFn = (headers: RequestHeaders) => number | null;
32
+
33
+ export type RequestHeadersMethodMap = {
34
+ [M in Method]?: RequestHeaders;
35
+ };
36
+
37
+ export interface PartialXhrState {
38
+ headers: RequestHeaders;
39
+ status: number;
40
+ statusText: string;
41
+ url: string;
42
+ }
43
+
44
+ export interface RequestRetryFnConfig {
45
+ error: RequestError;
46
+ headers: RequestHeaders;
47
+ currentRetryCount: number;
48
+ }
49
+
50
+ export interface RequestRetryFnResult {
51
+ retry: boolean;
52
+ delay?: number;
53
+ }
54
+
55
+ export type RequestRetryFn = (config: RequestRetryFnConfig) => RequestRetryFnResult;
56
+
57
+ export interface RequestConfig {
58
+ method: Method;
59
+ urlWithParams: string;
60
+ body?: unknown;
61
+ reportProgress?: boolean;
62
+ withCredentials?: boolean;
63
+ responseType?: 'arraybuffer' | 'blob' | 'json' | 'text';
64
+ headers?: RequestHeaders;
65
+ cacheAdapter?: CacheAdapterFn;
66
+ retryFn?: RequestRetryFn;
67
+ }
68
+
69
+ export type RequestProgress = {
70
+ current: number;
71
+ percentage?: number;
72
+ total?: number;
73
+ };
74
+
75
+ export type RequestEvent<Response = unknown> =
76
+ | {
77
+ type: 'start';
78
+ headers: RequestHeaders;
79
+ isRetry?: boolean;
80
+ retryNumber?: number;
81
+ retryDelay?: number;
82
+ }
83
+ | {
84
+ type: 'delay-retry';
85
+ headers: RequestHeaders;
86
+ retryNumber: number;
87
+ retryDelay: number;
88
+ }
89
+ | {
90
+ type: 'download-progress';
91
+ headers: RequestHeaders;
92
+ progress: RequestProgress;
93
+ partialText?: string;
94
+ }
95
+ | {
96
+ type: 'upload-progress';
97
+ headers: RequestHeaders;
98
+ progress: RequestProgress;
99
+ }
100
+ | {
101
+ type: 'success';
102
+ headers: RequestHeaders;
103
+ response: Response;
104
+ expiresInTimestamp?: number;
105
+ }
106
+ | {
107
+ type: 'failure';
108
+ headers: RequestHeaders;
109
+ error: RequestError;
110
+ }
111
+ | {
112
+ type: 'cancel';
113
+ headers: RequestHeaders;
114
+ };
115
+
116
+ export interface BuildQueryStringConfig {
117
+ /**
118
+ * Object notation to use for nested objects.
119
+ *
120
+ * @example
121
+ * // dot notation
122
+ * { foo: { bar: 'baz' } } => "foo.bar=baz"
123
+ *
124
+ * @example
125
+ * // bracket notation
126
+ * { foo: { bar: 'baz' } } => "foo[bar]=baz"
127
+ *
128
+ * @default 'bracket'
129
+ */
130
+ objectNotation?: 'dot' | 'bracket';
131
+
132
+ /**
133
+ * Whether to write array indexes in bracket notation.
134
+ *
135
+ * @example
136
+ * // true
137
+ * { foo: ['bar', 'baz'] } => "foo[0]=bar&foo[1]=baz"
138
+ *
139
+ * @example
140
+ * // false
141
+ * { foo: ['bar', 'baz'] } => "foo[]=bar&foo[]=baz"
142
+ *
143
+ * @default false
144
+ */
145
+ writeArrayIndexes?: boolean;
146
+
147
+ /**
148
+ * A list of values that should be ignored when building the query string.
149
+ *
150
+ * Also have a look at `ignoredValuesFns`.
151
+ *
152
+ * @default [undefined, null, Infinity, -Infinity]
153
+ */
154
+ ignoredValues?: Array<unknown>;
155
+
156
+ /**
157
+ * A list of functions that should be used to determine whether a value should be ignored when building the query string.
158
+ *
159
+ * Also have a look at `ignoredValues`.
160
+ *
161
+ * @default [isNaN, isEmptyString]
162
+ */
163
+ ignoredValuesFns?: Array<(value: unknown) => boolean>;
164
+ }