@onehat/data 1.22.13 → 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.13",
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",
@@ -1,7 +1,7 @@
1
1
  /** @module Property */
2
2
 
3
3
  import Property from './Property.js';
4
- import accounting from 'accounting-js';
4
+ import * as accounting from 'accounting-js';
5
5
  import _ from 'lodash';
6
6
 
7
7
  /**
@@ -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,
@@ -316,7 +327,23 @@ class AjaxRepository extends Repository {
316
327
  if (!this.hasBaseParam(name)) {
317
328
  return null;
318
329
  }
319
- return this._baseParams[name];
330
+
331
+ // Handle simple property access
332
+ if (this._baseParams.hasOwnProperty(name)) {
333
+ return this._baseParams[name];
334
+ }
335
+
336
+ // Handle array notation like "conditions[fleets__enterprise_id]"
337
+ const keys = name.split(/[\[\].]+/).filter(Boolean);
338
+ let current = this._baseParams;
339
+
340
+ for(const key of keys) {
341
+ if (!current || !current.hasOwnProperty(key)) {
342
+ return null;
343
+ }
344
+ current = current[key];
345
+ }
346
+ return current;
320
347
  }
321
348
 
322
349
  /**
@@ -332,7 +359,22 @@ class AjaxRepository extends Repository {
332
359
  * @param {string} name - Param name
333
360
  */
334
361
  hasParam(name) {
335
- return this._params.hasOwnProperty(name);
362
+ if (this._params.hasOwnProperty(name)) {
363
+ return true;
364
+ }
365
+
366
+ // Check for array notation
367
+ const keys = name.split(/[\[\].]+/).filter(Boolean);
368
+ let current = this._params,
369
+ key;
370
+
371
+ for(key of keys) {
372
+ if (!current || !current.hasOwnProperty(key)) {
373
+ return false;
374
+ }
375
+ current = current[key];
376
+ }
377
+ return true;
336
378
  }
337
379
 
338
380
  /**
@@ -439,6 +481,30 @@ class AjaxRepository extends Repository {
439
481
  this.throwError('No "get" api endpoint defined.');
440
482
  return;
441
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
+
442
508
  this.emit('beforeLoad'); // TODO: canceling beforeLoad will cancel the load operation
443
509
  this.markLoading();
444
510
 
@@ -454,16 +520,9 @@ class AjaxRepository extends Repository {
454
520
  this.resumeEvents();
455
521
  }
456
522
 
457
- if (!_.isNil(params) && _.isObject(params)) {
458
- this.setParams(params);
459
- }
460
-
461
- const
462
- repository = this,
463
- url = this.getModel() + '/' + this.api.get,
464
- data = _.merge({}, this._baseParams, this._params);
523
+ const repository = this;
465
524
 
466
- return this._send(this.methods.get, url, data)
525
+ return this._send(this.methods.get, url, data, { isLoadRequest: true, requestKey, })
467
526
  .then(result => {
468
527
  if (this.debugMode) {
469
528
  console.log('Response for ' + this.name, result);
@@ -519,9 +578,69 @@ class AjaxRepository extends Repository {
519
578
  })
520
579
  .finally(() => {
521
580
  this.markLoading(false);
581
+ if (!this.disableLimitToOnlyOneLoadRequest && !this.isUnique && requestKey) {
582
+ this._activeLoadRequests.delete(requestKey);
583
+ }
522
584
  });
523
585
  }
524
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
+
525
644
  showMore(params = {}, callback) {
526
645
  params.showMore = true;
527
646
  return this.load(params, callback);
@@ -1009,7 +1128,7 @@ class AjaxRepository extends Repository {
1009
1128
  * Fires off axios request to server
1010
1129
  * @private
1011
1130
  */
1012
- _send(method, url, data) {
1131
+ _send(method, url, data, options = {}) {
1013
1132
 
1014
1133
  if (!url) {
1015
1134
  this.throwError('No url submitted');
@@ -1021,12 +1140,19 @@ class AjaxRepository extends Repository {
1021
1140
  return;
1022
1141
  }
1023
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
+
1024
1150
  const headers = _.merge({
1025
1151
  'Content-Type': 'application/json',
1026
1152
  'Accept': 'application/json',
1027
- }, this.headers);
1153
+ }, this.headers, options.headers);
1028
1154
 
1029
- const options = {
1155
+ const axiosOptions = {
1030
1156
  url,
1031
1157
  method,
1032
1158
  baseURL: this.api.baseURL,
@@ -1035,22 +1161,34 @@ class AjaxRepository extends Repository {
1035
1161
  params: method === 'GET' ? data : null,
1036
1162
  data: method !== 'GET' ? qs.stringify(data) : null,
1037
1163
  timeout: this.timeout,
1164
+ signal,
1038
1165
  };
1039
1166
 
1040
1167
  if (this.debugMode) {
1041
- console.log(url, options);
1168
+ console.log(url, axiosOptions);
1042
1169
  }
1043
1170
 
1044
- this.lastSendOptions = options;
1171
+ this.lastSendOptions = axiosOptions;
1045
1172
 
1046
- return this.axios(options)
1173
+ return this.axios(axiosOptions)
1047
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
+
1048
1180
  if (this.debugMode) {
1049
1181
  console.log(url + ' error', error);
1050
1182
  console.log('response:', error.response);
1051
1183
  }
1052
1184
  this.throwError(error);
1053
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
+ }
1054
1192
  });
1055
1193
  }
1056
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