@astral/mobx-query 1.18.3 → 1.19.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,23 +1,61 @@
1
1
  import { AuxiliaryQuery } from '../AuxiliaryQuery';
2
2
  import { type DataStorage } from '../DataStorage';
3
3
  import { QueryContainer } from '../QueryContainer';
4
- import { type StatusStorage } from '../StatusStorage';
4
+ import { StatusStorage } from '../StatusStorage';
5
5
  import { type FetchPolicy, type QueryBaseActions, type Sync, type SyncParams } from '../types';
6
6
  export declare const DEFAULT_INFINITE_ITEMS_COUNT = 30;
7
- export type InfiniteParams = {
7
+ export type FetchMoreDirection = 'forward' | 'backward';
8
+ export type InfiniteFetchMoreOptions<TResult> = {
9
+ /**
10
+ * Направление для дозапроса данных
11
+ * @default 'forward'
12
+ */
13
+ direction?: FetchMoreDirection;
14
+ /**
15
+ * Колбэк, вызываемый при успешном запросе, подразумевается использование, для запоминания последней успешной позиции
16
+ */
17
+ onSuccess?: (params: {
18
+ offset: number;
19
+ cursor: TResult;
20
+ }) => void;
21
+ };
22
+ export type InfiniteParams<TResult> = {
23
+ /**
24
+ * Смещение, от которого требуется сделать запрос
25
+ * @default 0
26
+ */
8
27
  offset: number;
28
+ /**
29
+ * Количество запрашиваемых элементов
30
+ * @default 30
31
+ */
9
32
  count: number;
33
+ /**
34
+ * Элемент использующийся в качестве точки отсчета при курсорной пагинации.
35
+ * При движении вперед (direction: 'forward') это последний элемент текущего набора данных
36
+ * При движении назад (direction: 'backward') это первый элемент текущего набора данных.
37
+ * Передается в двух случаях:
38
+ * - при использовании fetchMore.
39
+ * - при самом первом запросе, если указан initialCursor.
40
+ * Во всех остальных запросах, включая первый запрос без initialCursor и повторный запрос после invalidate(), cursor будет undefined.
41
+ */
42
+ cursor?: Partial<TResult>;
43
+ /**
44
+ * Направление запроса, присутствует только при использовании fetchMore
45
+ */
46
+ direction?: FetchMoreDirection;
10
47
  };
11
48
  export type InfiniteDataStorage<TResult> = DataStorage<{
12
49
  data: TResult[];
13
50
  offset: number;
14
51
  isEndReached: boolean;
52
+ isStartReached: boolean;
15
53
  }>;
16
54
  /**
17
55
  * Исполнитель запроса, ожидается,
18
56
  * что будет использоваться что-то возвращающее массив данных
19
57
  */
20
- export type InfiniteExecutor<TResult> = (params: InfiniteParams) => Promise<TResult[]>;
58
+ export type InfiniteExecutor<TResult> = (params: InfiniteParams<TResult>) => Promise<TResult[]>;
21
59
  export type InfiniteQueryParams<TResult, TError, TIsBackground extends boolean = false> = {
22
60
  /**
23
61
  * Количество запрашиваемых элементов
@@ -54,6 +92,15 @@ export type InfiniteQueryParams<TResult, TError, TIsBackground extends boolean =
54
92
  * Колбэк, вызываемый при успешном завершении запроса, подразумевается использование, для подтверждения валидности данных, чтобы квери не был удален из памяти
55
93
  */
56
94
  submitValidity?: () => void;
95
+ /**
96
+ * Стартовое значение offset, от которого требуется сделать самый первый запрос.
97
+ * @default 0
98
+ */
99
+ initialOffset?: number;
100
+ /**
101
+ * Стартовый элемент, от которого требуется сделать самый первый запрос
102
+ */
103
+ initialCursor?: Partial<TResult>;
57
104
  };
58
105
  /**
59
106
  * Квери для работы с инфинити запросами,
@@ -85,7 +132,20 @@ export declare class InfiniteQuery<TResult, TError = void, TIsBackground extends
85
132
  * Колбэк, вызываемый при успешном завершении запроса, подразумевается использование, для подтверждения валидности данных, чтобы квери не был удален из памяти
86
133
  */
87
134
  private readonly submitValidity?;
88
- constructor(executor: InfiniteExecutor<TResult>, { incrementCount, onError, enabledAutoFetch, fetchPolicy, dataStorage, statusStorage, backgroundStatusStorage, submitValidity, }: InfiniteQueryParams<TResult, TError, TIsBackground>);
135
+ /**
136
+ * Хранилище статусов для процесса дозапроса данных
137
+ */
138
+ private fetchMoreStatusStorage;
139
+ /**
140
+ * Стартовый offset для запроса,
141
+ */
142
+ private initialOffset;
143
+ /**
144
+ * Стартовое значение курсора, от которого должен произойти первый запрос.
145
+ * Предполагается, что этот элемент должен содержать только id сущности.
146
+ */
147
+ private initialCursor?;
148
+ constructor(executor: InfiniteExecutor<TResult>, { incrementCount, initialOffset, initialCursor, onError, enabledAutoFetch, fetchPolicy, dataStorage, statusStorage, backgroundStatusStorage, submitValidity, }: InfiniteQueryParams<TResult, TError, TIsBackground>);
89
149
  private get isNetworkOnly();
90
150
  /**
91
151
  * Счетчик отступа для инфинити запроса
@@ -99,7 +159,11 @@ export declare class InfiniteQuery<TResult, TError = void, TIsBackground extends
99
159
  * Флаг того, что мы достигли предела запрашиваемых элементов
100
160
  */
101
161
  get isEndReached(): boolean;
102
- private calcIsEndReachedByResult;
162
+ /**
163
+ * Флаг того, что мы достигли самого начала запрашиваемых элементов
164
+ */
165
+ get isStartReached(): boolean;
166
+ private calcIsLimitReachedByResult;
103
167
  /**
104
168
  * Форс метод для установки данных
105
169
  */
@@ -107,15 +171,18 @@ export declare class InfiniteQuery<TResult, TError = void, TIsBackground extends
107
171
  /**
108
172
  * Метод для обогащения параметров текущими значениями для инфинити
109
173
  */
110
- private get infiniteExecutor();
174
+ private makeInfiniteExecutor;
111
175
  /**
112
176
  * Метод для инвалидации данных
113
177
  */
114
178
  invalidate: () => void;
179
+ private setOffset;
180
+ get fetchMoreStatus(): import("../StatusStorage").Statuses<TError>;
115
181
  /**
116
182
  * Метод для запроса следующего набора данных
183
+ * @param options - параметры дозапроса данных
117
184
  */
118
- fetchMore: () => void;
185
+ fetchMore: (options?: InfiniteFetchMoreOptions<TResult>) => void;
119
186
  /**
120
187
  * Синхронный метод получения данных
121
188
  */
@@ -4,28 +4,47 @@ exports.InfiniteQuery = exports.DEFAULT_INFINITE_ITEMS_COUNT = void 0;
4
4
  const mobx_1 = require("mobx");
5
5
  const AuxiliaryQuery_1 = require("../AuxiliaryQuery");
6
6
  const QueryContainer_1 = require("../QueryContainer");
7
+ const StatusStorage_1 = require("../StatusStorage");
7
8
  exports.DEFAULT_INFINITE_ITEMS_COUNT = 30;
8
9
  /**
9
10
  * Квери для работы с инфинити запросами,
10
11
  * которые должны быть закешированы,
11
12
  */
12
13
  class InfiniteQuery extends QueryContainer_1.QueryContainer {
13
- constructor(executor, { incrementCount = exports.DEFAULT_INFINITE_ITEMS_COUNT, onError, enabledAutoFetch, fetchPolicy, dataStorage, statusStorage, backgroundStatusStorage = null, submitValidity, }) {
14
+ constructor(executor, { incrementCount = exports.DEFAULT_INFINITE_ITEMS_COUNT, initialOffset = 0, initialCursor, onError, enabledAutoFetch, fetchPolicy, dataStorage, statusStorage, backgroundStatusStorage = null, submitValidity, }) {
14
15
  super(statusStorage, backgroundStatusStorage, new AuxiliaryQuery_1.AuxiliaryQuery(statusStorage, backgroundStatusStorage));
15
16
  this.executor = executor;
17
+ /**
18
+ * Хранилище статусов для процесса дозапроса данных
19
+ */
20
+ this.fetchMoreStatusStorage = new StatusStorage_1.StatusStorage();
21
+ /**
22
+ * Стартовый offset для запроса,
23
+ */
24
+ this.initialOffset = 0;
25
+ /**
26
+ * Стартовое значение курсора, от которого должен произойти первый запрос.
27
+ * Предполагается, что этот элемент должен содержать только id сущности.
28
+ */
29
+ this.initialCursor = undefined;
16
30
  /**
17
31
  * Обработчик успешного запроса, проверяет что мы достигли предела
18
32
  */
19
- this.submitSuccess = (result, isEndReached) => {
33
+ this.submitSuccess = (result, isLimitReached, isBackward) => {
20
34
  var _a;
21
35
  this.storage.setData((current) => ({
22
36
  offset: current === null || current === void 0 ? void 0 : current.offset,
23
37
  data: result,
24
- isEndReached,
38
+ isEndReached: isBackward
39
+ ? Boolean(current === null || current === void 0 ? void 0 : current.isEndReached)
40
+ : isLimitReached,
41
+ isStartReached: isBackward
42
+ ? isLimitReached
43
+ : Boolean(current === null || current === void 0 ? void 0 : current.isStartReached),
25
44
  }));
26
45
  (_a = this.submitValidity) === null || _a === void 0 ? void 0 : _a.call(this);
27
46
  };
28
- this.calcIsEndReachedByResult = (result) => {
47
+ this.calcIsLimitReachedByResult = (result) => {
29
48
  // убеждаемся что результат запроса действительно массив,
30
49
  // и если количество элементов в ответе меньше,
31
50
  // чем запрашивалось, значит у бэка их больше нет,
@@ -50,34 +69,72 @@ class InfiniteQuery extends QueryContainer_1.QueryContainer {
50
69
  this.submitSuccess(param, this.isEndReached);
51
70
  }
52
71
  };
72
+ /**
73
+ * Метод для обогащения параметров текущими значениями для инфинити
74
+ */
75
+ this.makeInfiniteExecutor = (offset, cursor, direction) => () => this.executor({
76
+ offset,
77
+ count: this.incrementCount,
78
+ cursor,
79
+ direction,
80
+ });
53
81
  /**
54
82
  * Метод для инвалидации данных
55
83
  */
56
84
  this.invalidate = () => {
57
85
  this.auxiliary.invalidate();
58
86
  };
87
+ this.setOffset = (offset) => {
88
+ this.storage.setData((current) => ({
89
+ offset,
90
+ data: current === null || current === void 0 ? void 0 : current.data,
91
+ isEndReached: Boolean(current === null || current === void 0 ? void 0 : current.isEndReached),
92
+ isStartReached: Boolean(current === null || current === void 0 ? void 0 : current.isStartReached),
93
+ }));
94
+ };
59
95
  /**
60
96
  * Метод для запроса следующего набора данных
97
+ * @param options - параметры дозапроса данных
61
98
  */
62
- this.fetchMore = () => {
99
+ this.fetchMore = (options) => {
100
+ var _a, _b;
101
+ const { direction = 'forward', onSuccess } = options !== null && options !== void 0 ? options : {};
102
+ const isBackward = direction === 'backward';
103
+ const isLimitReached = isBackward ? this.isStartReached : this.isEndReached;
63
104
  // если мы еще не достигли предела
64
- if (!this.isEndReached && this.storage.data) {
65
- // прибавляем к офсету число запрашиваемых элементов
66
- this.storage.setData((current) => {
67
- var _a;
68
- return ({
69
- offset: ((_a = current === null || current === void 0 ? void 0 : current.offset) !== null && _a !== void 0 ? _a : 0) + this.incrementCount,
70
- data: current === null || current === void 0 ? void 0 : current.data,
71
- isEndReached: Boolean(current === null || current === void 0 ? void 0 : current.isEndReached),
72
- });
73
- });
105
+ if (!isLimitReached && this.storage.data) {
106
+ this.fetchMoreStatusStorage.setStartLoading();
107
+ const offsetBeforeExecute = this.offset;
108
+ const increment = (isBackward ? -1 : 1) * this.incrementCount;
109
+ const cursor = isBackward
110
+ ? (_a = this.data) === null || _a === void 0 ? void 0 : _a[0]
111
+ : (_b = this.data) === null || _b === void 0 ? void 0 : _b[this.data.length - 1];
112
+ // прибавляем к offset число запрашиваемых элементов
113
+ const requestedOffset = Math.max(offsetBeforeExecute + increment, 0);
114
+ this.setOffset(requestedOffset);
74
115
  // запускаем запрос с последними параметрами, и флагом необходимости инкремента
75
116
  this.auxiliary
76
- .getUnifiedPromise(this.infiniteExecutor, (resData) => {
117
+ .getUnifiedPromise(this.makeInfiniteExecutor(requestedOffset, cursor, direction), (resData) => {
118
+ var _a;
119
+ this.fetchMoreStatusStorage.setSuccess();
120
+ const currentData = (_a = this.storage.data) === null || _a === void 0 ? void 0 : _a.data;
121
+ const result = isBackward
122
+ ? [...resData, ...currentData]
123
+ : [...currentData, ...resData];
124
+ this.submitSuccess(result, this.calcIsLimitReachedByResult(resData), isBackward);
125
+ onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess({
126
+ offset: requestedOffset,
127
+ cursor: cursor,
128
+ });
129
+ })
130
+ .catch((e) => {
77
131
  var _a;
78
- this.submitSuccess([...(_a = this.storage.data) === null || _a === void 0 ? void 0 : _a.data, ...resData], this.calcIsEndReachedByResult(resData));
132
+ this.fetchMoreStatusStorage.setError(e);
133
+ // в случае ошибки, откатываем значение offset к состоянию до дозапроса
134
+ this.setOffset(offsetBeforeExecute);
135
+ return (_a = this.defaultOnError) === null || _a === void 0 ? void 0 : _a.call(this, e);
79
136
  })
80
- .catch((e) => { var _a; return (_a = this.defaultOnError) === null || _a === void 0 ? void 0 : _a.call(this, e); });
137
+ .finally(this.fetchMoreStatusStorage.setEndLoading);
81
138
  }
82
139
  };
83
140
  /**
@@ -99,14 +156,15 @@ class InfiniteQuery extends QueryContainer_1.QueryContainer {
99
156
  */
100
157
  this.proceedSync = ({ onSuccess, onError, } = {}) => {
101
158
  this.storage.setData((current) => ({
102
- offset: 0,
159
+ offset: (this.isIdle && this.initialOffset) || 0,
103
160
  data: current === null || current === void 0 ? void 0 : current.data,
104
161
  isEndReached: false,
162
+ isStartReached: false,
105
163
  }));
106
164
  this.auxiliary
107
- .getUnifiedPromise(this.infiniteExecutor, (resData) => {
165
+ .getUnifiedPromise(this.makeInfiniteExecutor(this.offset, this.isIdle ? this.initialCursor : undefined), (resData) => {
108
166
  onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(resData);
109
- this.submitSuccess(resData, this.calcIsEndReachedByResult(resData));
167
+ this.submitSuccess(resData, this.calcIsLimitReachedByResult(resData));
110
168
  })
111
169
  .catch((e) => {
112
170
  var _a;
@@ -133,12 +191,15 @@ class InfiniteQuery extends QueryContainer_1.QueryContainer {
133
191
  return Promise.resolve((_a = this.storage.data) === null || _a === void 0 ? void 0 : _a.data);
134
192
  }
135
193
  this.storage.setData((current) => ({
136
- offset: 0,
194
+ offset: (this.isIdle && this.initialOffset) || 0,
137
195
  data: current === null || current === void 0 ? void 0 : current.data,
138
196
  isEndReached: false,
197
+ isStartReached: false,
139
198
  }));
140
- return this.auxiliary.getUnifiedPromise(this.infiniteExecutor, (data) => this.submitSuccess(data, this.calcIsEndReachedByResult(data)));
199
+ return this.auxiliary.getUnifiedPromise(this.makeInfiniteExecutor(this.offset, this.isIdle ? this.initialCursor : undefined), (data) => this.submitSuccess(data, this.calcIsLimitReachedByResult(data)));
141
200
  };
201
+ this.initialOffset = initialOffset;
202
+ this.initialCursor = initialCursor;
142
203
  this.storage = dataStorage;
143
204
  this.incrementCount = incrementCount;
144
205
  this.defaultOnError = onError;
@@ -146,15 +207,16 @@ class InfiniteQuery extends QueryContainer_1.QueryContainer {
146
207
  this.defaultFetchPolicy = fetchPolicy;
147
208
  this.submitValidity = submitValidity;
148
209
  (0, mobx_1.makeObservable)(this, {
210
+ fetchMoreStatus: mobx_1.computed,
149
211
  data: mobx_1.computed,
150
212
  computedData: mobx_1.computed,
151
- infiniteExecutor: mobx_1.computed,
152
213
  forceUpdate: mobx_1.action,
153
214
  async: mobx_1.action,
154
215
  sync: mobx_1.action,
155
216
  fetchMore: mobx_1.action,
156
217
  submitSuccess: mobx_1.action,
157
218
  isEndReached: mobx_1.computed,
219
+ isStartReached: mobx_1.computed,
158
220
  });
159
221
  }
160
222
  get isNetworkOnly() {
@@ -175,13 +237,14 @@ class InfiniteQuery extends QueryContainer_1.QueryContainer {
175
237
  return Boolean((_a = this.storage.data) === null || _a === void 0 ? void 0 : _a.isEndReached);
176
238
  }
177
239
  /**
178
- * Метод для обогащения параметров текущими значениями для инфинити
240
+ * Флаг того, что мы достигли самого начала запрашиваемых элементов
179
241
  */
180
- get infiniteExecutor() {
181
- return () => this.executor({
182
- offset: this.offset,
183
- count: this.incrementCount,
184
- });
242
+ get isStartReached() {
243
+ var _a;
244
+ return Boolean((_a = this.storage.data) === null || _a === void 0 ? void 0 : _a.isStartReached);
245
+ }
246
+ get fetchMoreStatus() {
247
+ return this.fetchMoreStatusStorage.statuses;
185
248
  }
186
249
  get computedData() {
187
250
  var _a;
@@ -111,8 +111,9 @@ class MobxQuery {
111
111
  */
112
112
  this.getCachedQuery = (key, createInstance, type, createParams) => {
113
113
  var _a, _b;
114
+ const enabledAutoFetch = (_a = createParams === null || createParams === void 0 ? void 0 : createParams.enabledAutoFetch) !== null && _a !== void 0 ? _a : this.defaultEnabledAutoFetch;
114
115
  const fetchPolicy = (createParams === null || createParams === void 0 ? void 0 : createParams.fetchPolicy) || this.defaultFetchPolicy;
115
- const keys = this.makeKeys(key, fetchPolicy, (_a = createParams === null || createParams === void 0 ? void 0 : createParams.isBackground) !== null && _a !== void 0 ? _a : false, type);
116
+ const keys = this.makeKeys(key, fetchPolicy, (_b = createParams === null || createParams === void 0 ? void 0 : createParams.isBackground) !== null && _b !== void 0 ? _b : false, type, enabledAutoFetch);
116
117
  const cachedQuery = this.queriesMap.get(keys.queryKeyHash);
117
118
  if (cachedQuery) {
118
119
  return cachedQuery;
@@ -120,7 +121,7 @@ class MobxQuery {
120
121
  const query = createInstance({
121
122
  onError: ((createParams === null || createParams === void 0 ? void 0 : createParams.onError) ||
122
123
  this.defaultErrorHandler),
123
- enabledAutoFetch: (_b = createParams === null || createParams === void 0 ? void 0 : createParams.enabledAutoFetch) !== null && _b !== void 0 ? _b : this.defaultEnabledAutoFetch,
124
+ enabledAutoFetch,
124
125
  fetchPolicy,
125
126
  dataStorage: this.queryDataStorageFactory.getStorage(keys.dataKeyHash),
126
127
  statusStorage: this.statusStorageFactory.getStorage(keys.statusKeyHash),
@@ -137,7 +138,7 @@ class MobxQuery {
137
138
  /**
138
139
  * Метод для создания ключей к внутренним хранилищам
139
140
  */
140
- this.makeKeys = (rootKey, fetchPolicy, isBackground, type) => {
141
+ this.makeKeys = (rootKey, fetchPolicy, isBackground, type, enabledAutoFetch) => {
141
142
  // C введением StrictMode в реакт 18, проявилась проблема,
142
143
  // что network-only квери, созданные в одном реакт компоненте,
143
144
  // создаются дважды (т.к. все хуки вызываются дважды)
@@ -151,7 +152,10 @@ class MobxQuery {
151
152
  // Этот кейс крайне маловероятен, и будет проявляться только в дев режиме с включенным StrictMode,
152
153
  // поэтому мы пренебрегаем решением этой проблемы, в угоду упрощения.
153
154
  const date = fetchPolicy === 'network-only' ? new Date().setMilliseconds(0) : null;
154
- const queryKey = [...rootKey, { fetchPolicy, date, isBackground, type }];
155
+ const queryKey = [
156
+ ...rootKey,
157
+ { fetchPolicy, date, isBackground, type, enabledAutoFetch },
158
+ ];
155
159
  const queryKeyHash = this.serialize(queryKey);
156
160
  const dataKeyHash = this.serialize([...rootKey, { type }]);
157
161
  const statusKeyHash = this.serialize([...rootKey, { type, date }]);
@@ -7,7 +7,7 @@ export type InfiniteQuerySetConfig = {
7
7
  };
8
8
  export type InfiniteQuerySetConfigurator<TParams extends any[], TResponse> = (...params: TParams) => {
9
9
  keys?: CacheKey[];
10
- execute: (infiniteParams: InfiniteParams) => Promise<TResponse[]>;
10
+ execute: (infiniteParams: InfiniteParams<TResponse>) => Promise<TResponse[]>;
11
11
  };
12
12
  /**
13
13
  * Набор infinite queries под одним ключем с целью повышения читаемости кода и удобства инвалидации по общему ключу
@@ -24,6 +24,10 @@ export declare class StatusStorage<TError> implements Statuses<TError> {
24
24
  */
25
25
  isSuccess: boolean;
26
26
  setStatues: (data: Statuses<TError>) => void;
27
+ setStartLoading: () => void;
28
+ setError: (error: TError) => void;
29
+ setSuccess: () => void;
30
+ setEndLoading: () => void;
27
31
  get statuses(): Statuses<TError>;
28
32
  }
29
33
  export declare class StatusStorageFactory extends StorageFactory<StatusStorage<any>> {
@@ -27,6 +27,24 @@ class StatusStorage {
27
27
  this.error = data.error;
28
28
  this.isSuccess = data.isSuccess;
29
29
  };
30
+ this.setStartLoading = () => {
31
+ this.isLoading = true;
32
+ this.isError = false;
33
+ this.isSuccess = false;
34
+ };
35
+ this.setError = (error) => {
36
+ this.isSuccess = false;
37
+ this.isError = true;
38
+ this.error = error;
39
+ };
40
+ this.setSuccess = () => {
41
+ this.error = undefined;
42
+ this.isError = false;
43
+ this.isSuccess = true;
44
+ };
45
+ this.setEndLoading = () => {
46
+ this.isLoading = false;
47
+ };
30
48
  (0, mobx_1.makeAutoObservable)(this);
31
49
  }
32
50
  get statuses() {
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "peerDependencies": {
11
11
  "mobx": "^6.0.0"
12
12
  },
13
- "version": "1.18.3",
13
+ "version": "1.19.1",
14
14
  "author": "Astral.Soft",
15
15
  "license": "MIT",
16
16
  "repository": {