@onehat/data 1.22.15 → 1.22.16

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/data",
3
- "version": "1.22.15",
3
+ "version": "1.22.16",
4
4
  "description": "JS data modeling package with adapters for many storage mediums.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -100,6 +100,11 @@ class AjaxRepository extends Repository {
100
100
  * @private
101
101
  */
102
102
  isOnline: true,
103
+
104
+ /**
105
+ * @member {boolean} disableLimitToOnlyOneLoadRequest - If true, disables automatic cancellation of duplicate load requests
106
+ */
107
+ disableLimitToOnlyOneLoadRequest: false,
103
108
 
104
109
  };
105
110
  _.merge(this, defaults, config);
@@ -119,6 +124,12 @@ class AjaxRepository extends Repository {
119
124
  * @private
120
125
  */
121
126
  this._params = {};
127
+
128
+ /**
129
+ * @member {Map} _activeLoadRequests - Map of active requests keyed by URL
130
+ * @private
131
+ */
132
+ this._activeLoadRequests = new Map();
122
133
 
123
134
  this._operations = {
124
135
  add: false,
@@ -470,6 +481,30 @@ class AjaxRepository extends Repository {
470
481
  this.throwError('No "get" api endpoint defined.');
471
482
  return;
472
483
  }
484
+
485
+
486
+
487
+ if (!_.isNil(params) && _.isObject(params)) {
488
+ this.setParams(params);
489
+ }
490
+ const
491
+ url = this.getModel() + '/' + this.api.get,
492
+ data = _.merge({}, this._baseParams, this._params),
493
+ requestKey = this._generateRequestKey(url, data);
494
+
495
+ if (!this.disableLimitToOnlyOneLoadRequest && !this.isUnique) {
496
+ if (this._activeLoadRequests.has(requestKey)) {
497
+ // Identical request already in progress, ignore this one
498
+ if (this.debugMode) {
499
+ console.log('Ignoring duplicate load request for', url, data);
500
+ }
501
+ return;
502
+ }
503
+
504
+ // Cancel any existing request for the same URL but different params
505
+ this._cancelExistingRequestsForUrl(url, requestKey);
506
+ }
507
+
473
508
  this.emit('beforeLoad'); // TODO: canceling beforeLoad will cancel the load operation
474
509
  this.markLoading();
475
510
 
@@ -485,16 +520,9 @@ class AjaxRepository extends Repository {
485
520
  this.resumeEvents();
486
521
  }
487
522
 
488
- if (!_.isNil(params) && _.isObject(params)) {
489
- this.setParams(params);
490
- }
491
-
492
- const
493
- repository = this,
494
- url = this.getModel() + '/' + this.api.get,
495
- data = _.merge({}, this._baseParams, this._params);
523
+ const repository = this;
496
524
 
497
- return this._send(this.methods.get, url, data)
525
+ return this._send(this.methods.get, url, data, { isLoadRequest: true, requestKey, })
498
526
  .then(result => {
499
527
  if (this.debugMode) {
500
528
  console.log('Response for ' + this.name, result);
@@ -550,9 +578,69 @@ class AjaxRepository extends Repository {
550
578
  })
551
579
  .finally(() => {
552
580
  this.markLoading(false);
581
+ if (!this.disableLimitToOnlyOneLoadRequest && !this.isUnique && requestKey) {
582
+ this._activeLoadRequests.delete(requestKey);
583
+ }
553
584
  });
554
585
  }
555
586
 
587
+ /**
588
+ * Generates a unique key for a request based on URL and parameters
589
+ * @param {string} url - The request URL
590
+ * @param {object} data - The request parameters
591
+ * @return {string} requestKey - Unique key for this URL+params combination
592
+ * @private
593
+ */
594
+ _generateRequestKey(url, data) {
595
+ const sortedData = this._sortObjectDeep(data);
596
+ return url + '::' + JSON.stringify(sortedData);
597
+ }
598
+
599
+ /**
600
+ * Deep sorts an object by keys to ensure consistent hashing
601
+ * @param {object} obj - Object to sort
602
+ * @return {object} sortedObj - Object with keys sorted recursively
603
+ * @private
604
+ */
605
+ _sortObjectDeep(obj) {
606
+ if (_.isArray(obj)) {
607
+ return obj.map(item => this._sortObjectDeep(item));
608
+ } else if (_.isPlainObject(obj)) {
609
+ const sortedObj = {};
610
+ const sortedKeys = Object.keys(obj).sort();
611
+ for (const key of sortedKeys) {
612
+ sortedObj[key] = this._sortObjectDeep(obj[key]);
613
+ }
614
+ return sortedObj;
615
+ }
616
+ return obj;
617
+ }
618
+
619
+ /**
620
+ * Cancels any existing requests for the same URL but different params
621
+ * @param {string} url - The request URL
622
+ * @param {string} currentRequestKey - The current request key to exclude from cancellation
623
+ * @private
624
+ */
625
+ _cancelExistingRequestsForUrl(url, currentRequestKey) {
626
+ const keysToCancel = [];
627
+
628
+ // Find all requests for the same URL but different params
629
+ for (const [requestKey, requestInfo] of this._activeLoadRequests.entries()) {
630
+ if (requestKey !== currentRequestKey && requestKey.startsWith(url + '::')) {
631
+ keysToCancel.push(requestKey);
632
+ if (requestInfo.controller) {
633
+ requestInfo.controller.abort();
634
+ }
635
+ }
636
+ }
637
+
638
+ // Remove cancelled requests from tracking
639
+ keysToCancel.forEach(key => {
640
+ this._activeLoadRequests.delete(key);
641
+ });
642
+ }
643
+
556
644
  showMore(params = {}, callback) {
557
645
  params.showMore = true;
558
646
  return this.load(params, callback);
@@ -1040,7 +1128,7 @@ class AjaxRepository extends Repository {
1040
1128
  * Fires off axios request to server
1041
1129
  * @private
1042
1130
  */
1043
- _send(method, url, data) {
1131
+ _send(method, url, data, options = {}) {
1044
1132
 
1045
1133
  if (!url) {
1046
1134
  this.throwError('No url submitted');
@@ -1052,12 +1140,19 @@ class AjaxRepository extends Repository {
1052
1140
  return;
1053
1141
  }
1054
1142
 
1143
+ const controller = new AbortController();
1144
+ const { signal } = controller;
1145
+
1146
+ if (options.isLoadRequest && !this.disableLimitToOnlyOneLoadRequest && !this.isUnique && options.requestKey) {
1147
+ this._activeLoadRequests.set(options.requestKey, { controller });
1148
+ }
1149
+
1055
1150
  const headers = _.merge({
1056
1151
  'Content-Type': 'application/json',
1057
1152
  'Accept': 'application/json',
1058
- }, this.headers);
1153
+ }, this.headers, options.headers);
1059
1154
 
1060
- const options = {
1155
+ const axiosOptions = {
1061
1156
  url,
1062
1157
  method,
1063
1158
  baseURL: this.api.baseURL,
@@ -1066,22 +1161,34 @@ class AjaxRepository extends Repository {
1066
1161
  params: method === 'GET' ? data : null,
1067
1162
  data: method !== 'GET' ? qs.stringify(data) : null,
1068
1163
  timeout: this.timeout,
1164
+ signal,
1069
1165
  };
1070
1166
 
1071
1167
  if (this.debugMode) {
1072
- console.log(url, options);
1168
+ console.log(url, axiosOptions);
1073
1169
  }
1074
1170
 
1075
- this.lastSendOptions = options;
1171
+ this.lastSendOptions = axiosOptions;
1076
1172
 
1077
- return this.axios(options)
1173
+ return this.axios(axiosOptions)
1078
1174
  .catch(error => {
1175
+ // Don't log or throw error if request was aborted
1176
+ if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
1177
+ return Promise.reject(new Error('Request cancelled'));
1178
+ }
1179
+
1079
1180
  if (this.debugMode) {
1080
1181
  console.log(url + ' error', error);
1081
1182
  console.log('response:', error.response);
1082
1183
  }
1083
1184
  this.throwError(error);
1084
1185
  return;
1186
+ })
1187
+ .finally(() => {
1188
+ // Clean up tracking for GET requests
1189
+ if (options.isLoadRequest && !this.disableLimitToOnlyOneLoadRequest && !this.isUnique && options.requestKey) {
1190
+ this._activeLoadRequests.delete(options.requestKey);
1191
+ }
1085
1192
  });
1086
1193
  }
1087
1194
 
@@ -97,7 +97,7 @@ class OneBuildRepository extends AjaxRepository {
97
97
  * Fires off axios request to server
98
98
  * @private
99
99
  */
100
- _send(method, url, data, headers) {
100
+ _send(method, url, data, options = {}) {
101
101
 
102
102
  if (!url) {
103
103
  this.throwError('No url submitted');
@@ -109,12 +109,20 @@ class OneBuildRepository extends AjaxRepository {
109
109
  return;
110
110
  }
111
111
 
112
+ // Create AbortController for this request
113
+ const controller = new AbortController();
114
+ const { signal } = controller;
115
+
116
+ if (options.isLoadRequest && !this.disableLimitToOnlyOneLoadRequest && !this.isUnique && options.requestKey) {
117
+ this._activeLoadRequests.set(options.requestKey, { controller });
118
+ }
119
+
112
120
  const mergedHeaders = _.merge({
113
121
  // 'Content-Type': 'application/json', // Stops axios from using 'application/x-www-form-urlencoded'
114
122
  Accept: 'application/json',
115
- }, this.headers, headers);
123
+ }, this.headers, options.headers);
116
124
 
117
- const options = {
125
+ const axiosOptions = {
118
126
  url,
119
127
  method,
120
128
  baseURL: this.api.baseURL,
@@ -123,16 +131,22 @@ class OneBuildRepository extends AjaxRepository {
123
131
  params: method === 'GET' ? data : null,
124
132
  data: method !== 'GET' ? qs.stringify(data) : null,
125
133
  timeout: this.timeout,
134
+ signal,
126
135
  };
127
136
 
128
137
  if (this.debugMode) {
129
- console.log('Sending ' + url, options);
138
+ console.log('Sending ' + url, axiosOptions);
130
139
  }
131
140
 
132
- this.lastSendOptions = options;
141
+ this.lastSendOptions = axiosOptions;
133
142
 
134
- return this.axios(options)
143
+ return this.axios(axiosOptions)
135
144
  .catch(error => {
145
+ // Don't log or throw error if request was aborted
146
+ if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
147
+ return Promise.reject(new Error('Request cancelled'));
148
+ }
149
+
136
150
  if (this.debugMode) {
137
151
  console.log(url + ' error', error);
138
152
  console.log('response:', error.response);
@@ -147,6 +161,11 @@ class OneBuildRepository extends AjaxRepository {
147
161
 
148
162
  this.throwError(error);
149
163
  return;
164
+ })
165
+ .finally(() => {
166
+ if (options.isLoadRequest && !this.disableLimitToOnlyOneLoadRequest && !this.isUnique && options.requestKey) {
167
+ this._activeLoadRequests.delete(options.requestKey);
168
+ }
150
169
  });
151
170
  }
152
171
 
@@ -216,6 +216,7 @@ export default class Repository extends EventEmitter {
216
216
 
217
217
  /**
218
218
  * @member {number} total - Total number of entities in remote storage that pass filters
219
+ * Example: "Showing 21-30 of 45" This would be 45
219
220
  */
220
221
  this.total = 0;
221
222