@xh/hoist 59.3.1 → 59.4.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.
- package/CHANGELOG.md +30 -2
- package/admin/differ/DifferModel.ts +5 -7
- package/appcontainer/AppContainerModel.ts +8 -10
- package/appcontainer/AppStateModel.ts +3 -2
- package/appcontainer/PageStateModel.ts +1 -2
- package/appcontainer/SizingModeModel.ts +2 -2
- package/cmp/ag-grid/AgGrid.ts +4 -3
- package/cmp/ag-grid/AgGridModel.ts +8 -9
- package/cmp/chart/Chart.ts +4 -3
- package/cmp/dataview/DataViewModel.ts +2 -2
- package/cmp/filter/FilterChooserModel.ts +5 -5
- package/cmp/grid/Grid.ts +2 -2
- package/cmp/grid/GridContextMenu.ts +2 -2
- package/cmp/grid/GridModel.ts +13 -21
- package/cmp/grid/Types.ts +6 -5
- package/cmp/grid/columns/Column.ts +12 -12
- package/cmp/grid/columns/ColumnGroup.ts +17 -6
- package/cmp/grid/helpers/GridCountLabel.ts +5 -4
- package/cmp/grid/impl/ColumnWidthCalculator.ts +3 -3
- package/cmp/grid/impl/GridPersistenceModel.ts +11 -4
- package/cmp/grid/renderers/MultiFieldRenderer.ts +28 -22
- package/cmp/grouping/GroupingChooserModel.ts +2 -2
- package/cmp/tab/TabContainerModel.ts +2 -2
- package/cmp/zoneGrid/ZoneGridModel.ts +52 -36
- package/cmp/zoneGrid/impl/ZoneGridPersistenceModel.ts +10 -4
- package/core/HoistBase.ts +44 -5
- package/core/XH.ts +3 -3
- package/core/elem.ts +2 -2
- package/core/impl/InstallServices.ts +2 -8
- package/core/load/LoadSupport.ts +3 -3
- package/core/model/HoistModel.ts +1 -1
- package/data/Store.ts +1 -1
- package/data/UrlStore.ts +3 -3
- package/data/filter/CompoundFilter.ts +5 -3
- package/data/filter/FieldFilter.ts +4 -3
- package/data/filter/Filter.ts +2 -3
- package/data/filter/FunctionFilter.ts +2 -1
- package/data/impl/RecordSet.ts +5 -5
- package/desktop/appcontainer/ToastSource.ts +1 -1
- package/desktop/cmp/rest/Actions.ts +15 -9
- package/desktop/cmp/treemap/TreeMap.ts +4 -8
- package/package.json +1 -1
- package/svc/AutoRefreshService.ts +3 -3
- package/svc/EnvironmentService.ts +1 -1
- package/svc/FetchService.ts +5 -5
- package/svc/GridAutosizeService.ts +4 -7
- package/svc/TrackService.ts +6 -6
- package/svc/WebSocketService.ts +14 -15
- package/utils/async/AsyncUtils.ts +3 -2
- package/utils/async/Timer.ts +32 -19
- package/utils/js/BrowserUtils.ts +8 -8
- package/utils/js/LangUtils.ts +10 -9
- package/utils/js/LogUtils.ts +66 -26
- package/utils/react/LayoutPropUtils.ts +3 -3
|
@@ -26,7 +26,8 @@ export class FunctionFilter extends Filter {
|
|
|
26
26
|
readonly testFn: FilterTestFn;
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Constructor - not typically called by apps - create
|
|
29
|
+
* Constructor - not typically called by apps - create via {@link parseFilter} instead.
|
|
30
|
+
* @internal
|
|
30
31
|
*/
|
|
31
32
|
constructor({key, testFn}: FunctionFilterSpec) {
|
|
32
33
|
super();
|
package/data/impl/RecordSet.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import equal from 'fast-deep-equal';
|
|
9
|
-
import {
|
|
9
|
+
import {logWarn, throwIf} from '@xh/hoist/utils/js';
|
|
10
10
|
import {maxBy, isNil} from 'lodash';
|
|
11
11
|
import {StoreRecord, StoreRecordId} from '../StoreRecord';
|
|
12
12
|
import {Store} from '../Store';
|
|
@@ -196,7 +196,7 @@ export class RecordSet {
|
|
|
196
196
|
remove.forEach(id => {
|
|
197
197
|
if (!newRecords.has(id)) {
|
|
198
198
|
missingRemoves++;
|
|
199
|
-
logDebug(`Attempted to remove non-existent record: ${id}
|
|
199
|
+
this.store.logDebug(`Attempted to remove non-existent record: ${id}`);
|
|
200
200
|
return;
|
|
201
201
|
}
|
|
202
202
|
this.gatherDescendantIds(id, allRemoves);
|
|
@@ -211,7 +211,7 @@ export class RecordSet {
|
|
|
211
211
|
existing = newRecords.get(id);
|
|
212
212
|
if (!existing) {
|
|
213
213
|
missingUpdates++;
|
|
214
|
-
logDebug(`Attempted to update non-existent record: ${id}
|
|
214
|
+
this.store.logDebug(`Attempted to update non-existent record: ${id}`);
|
|
215
215
|
return;
|
|
216
216
|
}
|
|
217
217
|
newRecords.set(id, rec);
|
|
@@ -230,9 +230,9 @@ export class RecordSet {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
if (missingRemoves > 0)
|
|
233
|
-
|
|
233
|
+
logWarn(`Failed to remove ${missingRemoves} records not found by id`, this);
|
|
234
234
|
if (missingUpdates > 0)
|
|
235
|
-
|
|
235
|
+
logWarn(`Failed to update ${missingUpdates} records not found by id`, this);
|
|
236
236
|
|
|
237
237
|
return new RecordSet(this.store, newRecords);
|
|
238
238
|
}
|
|
@@ -95,7 +95,7 @@ class ToastSourceLocalModel extends HoistModel {
|
|
|
95
95
|
*/
|
|
96
96
|
async getToasterAsync(position: ToasterPosition, container: HTMLElement) {
|
|
97
97
|
if (container && !isElement(container)) {
|
|
98
|
-
|
|
98
|
+
this.logWarn('Ignoring invalid containerRef for Toast - must be a DOM element');
|
|
99
99
|
container = null;
|
|
100
100
|
}
|
|
101
101
|
const className = `xh-toast-container ${container ? 'xh-toast-container--anchored' : ''}`;
|
|
@@ -6,15 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import '@xh/hoist/desktop/register';
|
|
9
|
+
import {GridModel} from '@xh/hoist/cmp/grid';
|
|
9
10
|
import {RecordActionSpec} from '@xh/hoist/data';
|
|
11
|
+
import {RestGridModel} from '@xh/hoist/desktop/cmp/rest/RestGridModel';
|
|
10
12
|
import {Icon} from '@xh/hoist/icon/Icon';
|
|
11
13
|
|
|
12
14
|
export const addAction: RecordActionSpec = {
|
|
13
15
|
text: 'Add',
|
|
14
16
|
icon: Icon.add(),
|
|
15
17
|
intent: 'success',
|
|
16
|
-
actionFn: ({gridModel}) => gridModel.
|
|
17
|
-
displayFn: ({gridModel}) => ({hidden: gridModel.
|
|
18
|
+
actionFn: ({gridModel}) => getRGM(gridModel).addRecord(),
|
|
19
|
+
displayFn: ({gridModel}) => ({hidden: getRGM(gridModel).readonly}),
|
|
18
20
|
testId: 'add-action-button'
|
|
19
21
|
};
|
|
20
22
|
|
|
@@ -23,8 +25,8 @@ export const editAction: RecordActionSpec = {
|
|
|
23
25
|
icon: Icon.edit(),
|
|
24
26
|
intent: 'primary',
|
|
25
27
|
recordsRequired: 1,
|
|
26
|
-
actionFn: ({record, gridModel}) => gridModel.
|
|
27
|
-
displayFn: ({gridModel}) => ({hidden: gridModel.
|
|
28
|
+
actionFn: ({record, gridModel}) => getRGM(gridModel).editRecord(record),
|
|
29
|
+
displayFn: ({gridModel}) => ({hidden: getRGM(gridModel).readonly}),
|
|
28
30
|
testId: 'edit-action-button'
|
|
29
31
|
};
|
|
30
32
|
|
|
@@ -32,7 +34,7 @@ export const viewAction: RecordActionSpec = {
|
|
|
32
34
|
text: 'View',
|
|
33
35
|
icon: Icon.search(),
|
|
34
36
|
recordsRequired: 1,
|
|
35
|
-
actionFn: ({record, gridModel}) => gridModel.
|
|
37
|
+
actionFn: ({record, gridModel}) => getRGM(gridModel).viewRecord(record),
|
|
36
38
|
testId: 'view-action-button'
|
|
37
39
|
};
|
|
38
40
|
|
|
@@ -40,8 +42,8 @@ export const cloneAction: RecordActionSpec = {
|
|
|
40
42
|
text: 'Clone',
|
|
41
43
|
icon: Icon.copy(),
|
|
42
44
|
recordsRequired: 1,
|
|
43
|
-
actionFn: ({record, gridModel}) => gridModel.
|
|
44
|
-
displayFn: ({gridModel}) => ({hidden: gridModel.
|
|
45
|
+
actionFn: ({record, gridModel}) => getRGM(gridModel).cloneRecord(record),
|
|
46
|
+
displayFn: ({gridModel}) => ({hidden: getRGM(gridModel).readonly}),
|
|
45
47
|
testId: 'clone-action-button'
|
|
46
48
|
};
|
|
47
49
|
|
|
@@ -51,8 +53,12 @@ export const deleteAction: RecordActionSpec = {
|
|
|
51
53
|
intent: 'danger',
|
|
52
54
|
recordsRequired: true,
|
|
53
55
|
displayFn: ({gridModel, record}) => ({
|
|
54
|
-
hidden: (record && record.id === null) || gridModel.
|
|
56
|
+
hidden: (record && record.id === null) || getRGM(gridModel).readonly // Hide this action if we are acting on a "new" record
|
|
55
57
|
}),
|
|
56
|
-
actionFn: ({gridModel}) => gridModel.
|
|
58
|
+
actionFn: ({gridModel}) => getRGM(gridModel).confirmDeleteRecords(),
|
|
57
59
|
testId: 'delete-action-button'
|
|
58
60
|
};
|
|
61
|
+
|
|
62
|
+
function getRGM(gridModel: GridModel): RestGridModel {
|
|
63
|
+
return gridModel.appData.restGridModel as RestGridModel;
|
|
64
|
+
}
|
|
@@ -22,7 +22,7 @@ import {mask} from '@xh/hoist/desktop/cmp/mask';
|
|
|
22
22
|
import '@xh/hoist/desktop/register';
|
|
23
23
|
import {Highcharts} from '@xh/hoist/kit/highcharts';
|
|
24
24
|
import {wait} from '@xh/hoist/promise';
|
|
25
|
-
import {logWithDebug
|
|
25
|
+
import {logWithDebug} from '@xh/hoist/utils/js';
|
|
26
26
|
import {
|
|
27
27
|
createObservableRef,
|
|
28
28
|
getLayoutProps,
|
|
@@ -193,13 +193,9 @@ class TreeMapLocalModel extends HoistModel {
|
|
|
193
193
|
if (parentDims.width === 0 || parentDims.height === 0) return;
|
|
194
194
|
|
|
195
195
|
assign(config.chart, parentDims, {renderTo: chartElem});
|
|
196
|
-
withDebug(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
this.chart = Highcharts.chart(config);
|
|
200
|
-
},
|
|
201
|
-
this
|
|
202
|
-
);
|
|
196
|
+
this.withDebug(['Creating new TreeMap', `${newData.length} records`], () => {
|
|
197
|
+
this.chart = Highcharts.chart(config);
|
|
198
|
+
});
|
|
203
199
|
}
|
|
204
200
|
|
|
205
201
|
@logWithDebug
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {HoistService, managed, XH} from '@xh/hoist/core';
|
|
8
8
|
import {Timer} from '@xh/hoist/utils/async';
|
|
9
9
|
import {olderThan, ONE_SECOND, SECONDS} from '@xh/hoist/utils/datetime';
|
|
10
|
-
import {
|
|
10
|
+
import {withDefault} from '@xh/hoist/utils/js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Service to triggers an app-wide auto-refresh (if enabled, on a configurable interval) via the
|
|
@@ -58,7 +58,7 @@ export class AutoRefreshService extends HoistService {
|
|
|
58
58
|
// Implementation
|
|
59
59
|
//------------------------
|
|
60
60
|
private async onTimerAsync() {
|
|
61
|
-
if (!this.enabled ||
|
|
61
|
+
if (!this.enabled || !XH.pageIsVisible) return;
|
|
62
62
|
|
|
63
63
|
// Wait interval after lastCompleted -- this prevents extra refreshes if user refreshes
|
|
64
64
|
// manually, or loading slow. Note auto-loads skipped if any load in progress.
|
|
@@ -69,7 +69,7 @@ export class AutoRefreshService extends HoistService {
|
|
|
69
69
|
pendingLoad = lastRequested && lastRequested > lastCompleted;
|
|
70
70
|
|
|
71
71
|
if (!pendingLoad && olderThan(last, this.interval * SECONDS)) {
|
|
72
|
-
logDebug('Triggering application auto-refresh.'
|
|
72
|
+
this.logDebug('Triggering application auto-refresh.');
|
|
73
73
|
await ctx.autoRefreshAsync();
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -125,7 +125,7 @@ export class EnvironmentService extends HoistService {
|
|
|
125
125
|
} else if (mode === 'forceReload') {
|
|
126
126
|
XH.suspendApp({
|
|
127
127
|
reason: 'APP_UPDATE',
|
|
128
|
-
message: `A new version of ${XH.clientAppName} is available
|
|
128
|
+
message: `A new version of ${XH.clientAppName} is now available (${appVersion}) and requires an immediate update.`
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
}
|
package/svc/FetchService.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2023 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {HoistService, XH, Exception, PlainObject,
|
|
7
|
+
import {HoistService, XH, Exception, PlainObject, FetchResponse, LoadSpec} from '@xh/hoist/core';
|
|
8
8
|
import {isLocalDate, SECONDS, ONE_MINUTE, olderThan} from '@xh/hoist/utils/datetime';
|
|
9
9
|
import {throwIf} from '@xh/hoist/utils/js';
|
|
10
10
|
import {StatusCodes} from 'http-status-codes';
|
|
@@ -66,7 +66,7 @@ export class FetchService extends HoistService {
|
|
|
66
66
|
* Set default headers to be sent with all subsequent requests.
|
|
67
67
|
* @param headers - to be sent with all fetch requests, or a function to generate.
|
|
68
68
|
*/
|
|
69
|
-
setDefaultHeaders(headers:
|
|
69
|
+
setDefaultHeaders(headers: PlainObject | ((arg: FetchOptions) => PlainObject)) {
|
|
70
70
|
this.defaultHeaders = headers;
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -328,9 +328,9 @@ export interface FetchOptions {
|
|
|
328
328
|
|
|
329
329
|
/**
|
|
330
330
|
* Data to send in the request body (for POSTs/PUTs of JSON).
|
|
331
|
-
* When using `fetch`, provide a string. Otherwise, provide a
|
|
331
|
+
* When using `fetch`, provide a string. Otherwise, provide a JSON Serializable object
|
|
332
332
|
*/
|
|
333
|
-
body?:
|
|
333
|
+
body?: any;
|
|
334
334
|
|
|
335
335
|
/**
|
|
336
336
|
* Parameters to encode and append as a query string, or send with the request body
|
|
@@ -360,7 +360,7 @@ export interface FetchOptions {
|
|
|
360
360
|
* Optional metadata about the underlying request. Passed through for downstream processing by
|
|
361
361
|
* utils such as {@link ExceptionHandler}.
|
|
362
362
|
*/
|
|
363
|
-
loadSpec?:
|
|
363
|
+
loadSpec?: LoadSpec;
|
|
364
364
|
|
|
365
365
|
/**
|
|
366
366
|
* Options to pass to the underlying fetch request.
|
|
@@ -60,24 +60,21 @@ export class GridAutosizeService extends HoistService {
|
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
if (!requiredWidths) {
|
|
63
|
-
|
|
63
|
+
this.logDebug('Autosize aborted, grid data is obsolete.');
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
runInAction(() => {
|
|
68
68
|
// 4) Set columns to their required widths.
|
|
69
69
|
gridModel.applyColumnStateChanges(requiredWidths);
|
|
70
|
-
|
|
71
|
-
`Column widths autosized via GridAutosizeService (${records.length} records)`,
|
|
72
|
-
requiredWidths
|
|
73
|
-
);
|
|
70
|
+
this.logDebug(`Auto-sized columns`, `${records.length} records`, requiredWidths);
|
|
74
71
|
|
|
75
72
|
// 5) Grow columns to fill any remaining space, if enabled.
|
|
76
73
|
const {fillMode} = options;
|
|
77
74
|
if (fillMode && fillMode !== 'none') {
|
|
78
75
|
const fillWidths = this.calcFillWidths(gridModel, colIds, fillMode);
|
|
79
76
|
gridModel.applyColumnStateChanges(fillWidths);
|
|
80
|
-
|
|
77
|
+
this.logDebug('Auto-sized columns using fillMode', fillWidths);
|
|
81
78
|
}
|
|
82
79
|
});
|
|
83
80
|
}
|
|
@@ -170,7 +167,7 @@ export class GridAutosizeService extends HoistService {
|
|
|
170
167
|
available = agApi?.gridPanel?.eBodyViewport?.clientWidth;
|
|
171
168
|
|
|
172
169
|
if (!agApi || !isFinite(available)) {
|
|
173
|
-
|
|
170
|
+
this.logWarn('Grid not rendered - unable to fill columns.');
|
|
174
171
|
return [];
|
|
175
172
|
}
|
|
176
173
|
|
package/svc/TrackService.ts
CHANGED
|
@@ -97,19 +97,19 @@ export class TrackService extends HoistService {
|
|
|
97
97
|
|
|
98
98
|
const {maxDataLength} = this.conf;
|
|
99
99
|
if (params.data?.length > maxDataLength) {
|
|
100
|
-
|
|
101
|
-
`
|
|
100
|
+
this.logWarn(
|
|
101
|
+
`Track log includes ${params.data.length} chars of JSON data`,
|
|
102
|
+
`exceeds limit of ${maxDataLength}`,
|
|
103
|
+
'data will not be persisted',
|
|
102
104
|
options.data
|
|
103
105
|
);
|
|
104
106
|
params.data = null;
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
const elapsedStr = params.elapsed != null ? `${params.elapsed}ms` : null,
|
|
108
|
-
|
|
109
|
-
.filter(it => it != null)
|
|
110
|
-
.join(' | ');
|
|
110
|
+
consoleMsgs = [params.category, params.msg, elapsedStr].filter(it => it != null);
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
this.logInfo(...consoleMsgs);
|
|
113
113
|
|
|
114
114
|
await XH.fetchJson({url: 'xh/track', params});
|
|
115
115
|
} catch (e) {
|
package/svc/WebSocketService.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {HoistService, XH} from '@xh/hoist/core';
|
|
8
8
|
import {Icon} from '@xh/hoist/icon';
|
|
9
|
-
import {action,
|
|
9
|
+
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
10
10
|
import {Timer} from '@xh/hoist/utils/async';
|
|
11
11
|
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
12
12
|
import {throwIf} from '@xh/hoist/utils/js';
|
|
@@ -55,11 +55,11 @@ export class WebSocketService extends HoistService {
|
|
|
55
55
|
return !!this.channelKey;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/**
|
|
58
|
+
/** Set to true to log all sent/received messages - very chatty. */
|
|
59
59
|
logMessages: boolean = false;
|
|
60
60
|
|
|
61
|
-
private _timer;
|
|
62
|
-
private _socket;
|
|
61
|
+
private _timer: Timer;
|
|
62
|
+
private _socket: WebSocket;
|
|
63
63
|
private _subsByTopic = {};
|
|
64
64
|
|
|
65
65
|
enabled: boolean = XH.appSpec.webSocketsEnabled;
|
|
@@ -72,9 +72,8 @@ export class WebSocketService extends HoistService {
|
|
|
72
72
|
override async initAsync() {
|
|
73
73
|
if (!this.enabled) return;
|
|
74
74
|
if (XH.environmentService.get('webSocketsEnabled') === false) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
'Please adjust your server-side configuration to use WebSockets.'
|
|
75
|
+
this.logError(
|
|
76
|
+
`WebSockets enabled on this client app but disabled on server. Adjust your server-side config.`
|
|
78
77
|
);
|
|
79
78
|
this.enabled = false;
|
|
80
79
|
return;
|
|
@@ -151,7 +150,7 @@ export class WebSocketService extends HoistService {
|
|
|
151
150
|
};
|
|
152
151
|
this._socket = s;
|
|
153
152
|
} catch (e) {
|
|
154
|
-
|
|
153
|
+
this.logError('Failure creating WebSocket', e);
|
|
155
154
|
}
|
|
156
155
|
|
|
157
156
|
this.updateConnectedStatus();
|
|
@@ -170,7 +169,7 @@ export class WebSocketService extends HoistService {
|
|
|
170
169
|
if (this.connected) {
|
|
171
170
|
this.sendMessage({topic: this.HEARTBEAT_TOPIC, data: 'ping'});
|
|
172
171
|
} else {
|
|
173
|
-
|
|
172
|
+
this.logWarn('Heartbeat found websocket not connected - attempting to reconnect...');
|
|
174
173
|
this.disconnect();
|
|
175
174
|
this.connect();
|
|
176
175
|
}
|
|
@@ -185,17 +184,17 @@ export class WebSocketService extends HoistService {
|
|
|
185
184
|
// Socket events impl
|
|
186
185
|
//------------------------
|
|
187
186
|
onOpen(ev) {
|
|
188
|
-
|
|
187
|
+
this.logDebug('WebSocket connection opened', ev);
|
|
189
188
|
this.updateConnectedStatus();
|
|
190
189
|
}
|
|
191
190
|
|
|
192
191
|
onClose(ev) {
|
|
193
|
-
|
|
192
|
+
this.logDebug('WebSocket connection closed', ev);
|
|
194
193
|
this.updateConnectedStatus();
|
|
195
194
|
}
|
|
196
195
|
|
|
197
196
|
onError(ev) {
|
|
198
|
-
|
|
197
|
+
this.logError('WebSocket connection error', ev);
|
|
199
198
|
this.updateConnectedStatus();
|
|
200
199
|
}
|
|
201
200
|
|
|
@@ -221,7 +220,7 @@ export class WebSocketService extends HoistService {
|
|
|
221
220
|
|
|
222
221
|
this.notifySubscribers(msg);
|
|
223
222
|
} catch (e) {
|
|
224
|
-
|
|
223
|
+
this.logError('Error decoding websocket message', rawMsg, e);
|
|
225
224
|
}
|
|
226
225
|
this.updateConnectedStatus();
|
|
227
226
|
}
|
|
@@ -236,7 +235,7 @@ export class WebSocketService extends HoistService {
|
|
|
236
235
|
try {
|
|
237
236
|
sub.fn(message);
|
|
238
237
|
} catch (e) {
|
|
239
|
-
|
|
238
|
+
this.logError(`Handler for topic ${message.topic} threw`, e);
|
|
240
239
|
}
|
|
241
240
|
});
|
|
242
241
|
}
|
|
@@ -287,7 +286,7 @@ export class WebSocketService extends HoistService {
|
|
|
287
286
|
}
|
|
288
287
|
|
|
289
288
|
maybeLogMessage(...args) {
|
|
290
|
-
if (this.logMessages)
|
|
289
|
+
if (this.logMessages) this.logDebug(args);
|
|
291
290
|
}
|
|
292
291
|
}
|
|
293
292
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2023 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {XH} from '@xh/hoist/core';
|
|
7
8
|
import {wait} from '@xh/hoist/promise';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -15,7 +16,7 @@ import {wait} from '@xh/hoist/promise';
|
|
|
15
16
|
* allowing ongoing rendering of UI updates (e.g. load masks) and generally keeping the browser
|
|
16
17
|
* event loop running.
|
|
17
18
|
*
|
|
18
|
-
* Note that if the
|
|
19
|
+
* Note that if the content tab is hidden (i.e. `!XH.pageIsVisible`) this loop will be executed
|
|
19
20
|
* without pauses. In this case the pauses would be unduly large due to throttling of the event
|
|
20
21
|
* loop by the browser, and there is no user benefit to avoiding blocking the main thread.
|
|
21
22
|
*
|
|
@@ -55,7 +56,7 @@ export async function whileAsync(
|
|
|
55
56
|
const {waitAfter = 50, waitFor = 0} = opts ?? {};
|
|
56
57
|
|
|
57
58
|
// Fallback to basic loop when doc hidden: no user benefit, and throttling causes outsize waits
|
|
58
|
-
if (
|
|
59
|
+
if (!XH.pageIsVisible) {
|
|
59
60
|
while (conditionFn()) fn();
|
|
60
61
|
return;
|
|
61
62
|
}
|
package/utils/async/Timer.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {XH} from '@xh/hoist/core';
|
|
8
8
|
import {wait} from '@xh/hoist/promise';
|
|
9
9
|
import {MILLISECONDS, MINUTES, olderThan} from '@xh/hoist/utils/datetime';
|
|
10
|
-
import {throwIf} from '@xh/hoist/utils/js';
|
|
10
|
+
import {logWarn, throwIf} from '@xh/hoist/utils/js';
|
|
11
11
|
import {isBoolean, isFinite, isFunction, isNil, isString, pull} from 'lodash';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -15,19 +15,20 @@ import {isBoolean, isFinite, isFunction, isNil, isString, pull} from 'lodash';
|
|
|
15
15
|
* Promise-aware recurring task timer for use by framework and applications.
|
|
16
16
|
*
|
|
17
17
|
* This object is designed to be robust across failing tasks, and never to re-run the task
|
|
18
|
-
* simultaneously, unless in the case of a timeout.
|
|
19
|
-
*
|
|
18
|
+
* simultaneously, unless in the case of a timeout. Callers can optionally specify the duration
|
|
19
|
+
* of asynchronous tasks by returning a Promise from runFn.
|
|
20
20
|
*
|
|
21
|
-
* This object seeks to mirror the API and semantics of
|
|
22
|
-
* as
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* This object seeks to mirror the API and semantics of `Timer.groovy` from Hoist Core as closely
|
|
22
|
+
* as possible. However, there are important differences due to the synchronous nature of
|
|
23
|
+
* javascript. In particular, there is no support for `runImmediatelyAndBlock`, and the `timeout`
|
|
24
|
+
* argument will not be able to interrupt synchronous activity of the runFn.
|
|
25
25
|
*
|
|
26
|
-
* All public properties should be considered read-only.
|
|
27
|
-
* of this
|
|
26
|
+
* All public properties should be considered read-only.
|
|
27
|
+
* See `setInterval()` to change the interval of this Timer dynamically.
|
|
28
28
|
*/
|
|
29
29
|
export class Timer {
|
|
30
30
|
static _timers: Timer[] = [];
|
|
31
|
+
static MIN_INTERVAL_MS = 500;
|
|
31
32
|
|
|
32
33
|
runFn: () => any = null;
|
|
33
34
|
interval: number | (() => number) = null;
|
|
@@ -41,6 +42,8 @@ export class Timer {
|
|
|
41
42
|
isRunning: boolean = false;
|
|
42
43
|
lastRun: Date = null;
|
|
43
44
|
|
|
45
|
+
private warnedIntervals = new Set();
|
|
46
|
+
|
|
44
47
|
/** Create a new Timer. */
|
|
45
48
|
static create({
|
|
46
49
|
runFn,
|
|
@@ -67,9 +70,7 @@ export class Timer {
|
|
|
67
70
|
this._timers = [];
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
/**
|
|
71
|
-
* Permanently cancel this timer.
|
|
72
|
-
*/
|
|
73
|
+
/** Permanently cancel this timer. */
|
|
73
74
|
cancel() {
|
|
74
75
|
this.cancelInternal();
|
|
75
76
|
pull(Timer._timers, this);
|
|
@@ -77,7 +78,6 @@ export class Timer {
|
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
80
|
* Change the interval of this timer.
|
|
80
|
-
*
|
|
81
81
|
* @param interval - ms to wait between runs or any value `<=0` to pause the timer.
|
|
82
82
|
*/
|
|
83
83
|
setInterval(interval: number) {
|
|
@@ -94,7 +94,10 @@ export class Timer {
|
|
|
94
94
|
this.intervalUnits = args.intervalUnits;
|
|
95
95
|
this.timeoutUnits = args.timeoutUnits;
|
|
96
96
|
this.delay = this.parseDelay(args.delay);
|
|
97
|
-
throwIf(
|
|
97
|
+
throwIf(
|
|
98
|
+
this.interval == null || this.runFn == null,
|
|
99
|
+
'Missing required arguments for Timer - both interval and runFn must be specified.'
|
|
100
|
+
);
|
|
98
101
|
|
|
99
102
|
wait(this.delay).then(() => this.heartbeatAsync());
|
|
100
103
|
}
|
|
@@ -133,18 +136,28 @@ export class Timer {
|
|
|
133
136
|
return isString(val) ? () => XH.configService.get(val) : val;
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
private parseDelay(val): number {
|
|
139
|
+
private parseDelay(val: number | boolean): number {
|
|
137
140
|
if (isBoolean(val)) return val ? this.intervalMs : 0;
|
|
138
141
|
return isFinite(val) ? val : 0;
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
private get intervalMs() {
|
|
142
|
-
const {interval, intervalUnits} = this
|
|
145
|
+
const {interval, intervalUnits, warnedIntervals} = this,
|
|
146
|
+
min = Timer.MIN_INTERVAL_MS;
|
|
147
|
+
|
|
143
148
|
if (isNil(interval)) return null;
|
|
149
|
+
|
|
144
150
|
let ret = (isFunction(interval) ? interval() : interval) * intervalUnits;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
ret
|
|
151
|
+
|
|
152
|
+
if (ret > 0 && ret < min) {
|
|
153
|
+
if (!warnedIntervals.has(ret)) {
|
|
154
|
+
warnedIntervals.add(ret);
|
|
155
|
+
logWarn(
|
|
156
|
+
`Interval of ${ret}ms requested - forcing to min interval of ${min}ms.`,
|
|
157
|
+
this
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
ret = min;
|
|
148
161
|
}
|
|
149
162
|
return ret;
|
|
150
163
|
}
|
package/utils/js/BrowserUtils.ts
CHANGED
|
@@ -10,8 +10,7 @@ import {pick} from 'lodash';
|
|
|
10
10
|
* Extract information (if available) about the client browser's window, screen, and network speed.
|
|
11
11
|
*/
|
|
12
12
|
export function getClientDeviceInfo() {
|
|
13
|
-
const data: any = pick(
|
|
14
|
-
window,
|
|
13
|
+
const data: any = pick(window, [
|
|
15
14
|
'screen',
|
|
16
15
|
'devicePixelRatio',
|
|
17
16
|
'screenX',
|
|
@@ -20,10 +19,10 @@ export function getClientDeviceInfo() {
|
|
|
20
19
|
'innerHeight',
|
|
21
20
|
'outerWidth',
|
|
22
21
|
'outerHeight'
|
|
23
|
-
);
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
24
|
if (data.screen) {
|
|
25
|
-
data.screen = pick(
|
|
26
|
-
data.screen,
|
|
25
|
+
data.screen = pick(data.screen, [
|
|
27
26
|
'availWidth',
|
|
28
27
|
'availHeight',
|
|
29
28
|
'width',
|
|
@@ -33,14 +32,15 @@ export function getClientDeviceInfo() {
|
|
|
33
32
|
'availLeft',
|
|
34
33
|
'availTop',
|
|
35
34
|
'orientation'
|
|
36
|
-
);
|
|
35
|
+
]);
|
|
37
36
|
if (data.screen.orientation) {
|
|
38
|
-
data.screen.orientation = pick(data.screen.orientation, 'angle', 'type');
|
|
37
|
+
data.screen.orientation = pick(data.screen.orientation, ['angle', 'type']);
|
|
39
38
|
}
|
|
40
39
|
}
|
|
40
|
+
|
|
41
41
|
const nav = window.navigator as any;
|
|
42
42
|
if (nav.connection) {
|
|
43
|
-
data.connection = pick(nav.connection, 'downlink', 'effectiveType', 'rtt');
|
|
43
|
+
data.connection = pick(nav.connection, ['downlink', 'effectiveType', 'rtt']);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
return data;
|
package/utils/js/LangUtils.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
isEmpty,
|
|
13
13
|
isFunction,
|
|
14
14
|
isObject,
|
|
15
|
-
|
|
15
|
+
isPlainObject,
|
|
16
16
|
isUndefined,
|
|
17
17
|
mixin,
|
|
18
18
|
uniq,
|
|
@@ -53,22 +53,23 @@ export function withDefault<T>(...args: T[]): T {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* Recursively freeze an object, preventing future modifications.
|
|
57
|
-
*
|
|
58
|
-
* freezable without side effects. This avoids freezing other types of objects where this routine
|
|
56
|
+
* Recursively freeze an object, preventing future modifications. Only the specific declared
|
|
57
|
+
* input types will be frozen. This avoids freezing other types of objects where this routine
|
|
59
58
|
* could be problematic - e.g. application or library classes (such as `moment`!) which rely on
|
|
60
59
|
* their internal state remaining mutable to function.
|
|
61
60
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
export function deepFreeze<
|
|
62
|
+
T extends Record<string, unknown> | Array<unknown> | Map<unknown, unknown> | Set<unknown>
|
|
63
|
+
>(obj: T): Readonly<T> {
|
|
64
|
+
if (!(isPlainObject(obj) || isArray(obj) || obj instanceof Map || obj instanceof Set)) {
|
|
65
|
+
return obj;
|
|
66
|
+
}
|
|
65
67
|
|
|
66
68
|
const propNames = Object.getOwnPropertyNames(obj);
|
|
67
69
|
for (const name of propNames) {
|
|
68
70
|
deepFreeze(obj[name]);
|
|
69
71
|
}
|
|
70
|
-
|
|
71
|
-
return Object.freeze(obj);
|
|
72
|
+
return Object.freeze<T>(obj);
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
/**
|