@xh/hoist 86.0.0 → 86.0.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.
- package/CHANGELOG.md +15 -0
- package/build/types/svc/PrefService.d.ts +4 -3
- package/build/types/svc/TraceService.d.ts +8 -6
- package/build/types/svc/TrackService.d.ts +5 -3
- package/build/types/svc/impl/Fetch.d.ts +13 -0
- package/build/types/utils/js/TestUtils.d.ts +1 -1
- package/build/types/utils/react/LayoutPropUtils.d.ts +1 -1
- package/build/types/utils/react/ReactUtils.d.ts +1 -1
- package/desktop/cmp/filter/FilterChooser.scss +0 -5
- package/package.json +4 -4
- package/svc/PrefService.ts +20 -10
- package/svc/TraceService.ts +53 -41
- package/svc/TrackService.ts +21 -10
- package/svc/impl/Fetch.ts +33 -0
- package/utils/js/ClipboardUtils.ts +2 -2
- package/utils/js/TestUtils.ts +1 -1
- package/utils/react/LayoutPropUtils.ts +1 -1
- package/utils/react/ReactUtils.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
## 86.0.1 - 2026-06-16
|
|
5
|
+
|
|
6
|
+
### 🐞 Bug Fixes
|
|
7
|
+
|
|
8
|
+
* Fixed a circular import introduced in v86 that caused problems for apps using `@ComputeOnce`.
|
|
9
|
+
* Fixed a regression to favorites icon size for `FilterChooser.`
|
|
10
|
+
|
|
11
|
+
### ⚙️ Technical
|
|
12
|
+
|
|
13
|
+
* `TrackService`, `PrefService`, and `TraceService` now flush their pending entries reliably when
|
|
14
|
+
the page is hidden or unloaded, reacting to `XH.pageState` and issuing the flush via
|
|
15
|
+
`fetch({keepalive: true})` (replacing the less reliable `beforeunload` + normal-fetch approach).
|
|
16
|
+
|
|
17
|
+
|
|
3
18
|
## 86.0.0 - 2026-06-12
|
|
4
19
|
|
|
5
20
|
### 💥 Breaking Changes (upgrade difficulty: 🟠 MEDIUM - library upgrades + component API changes)
|
|
@@ -58,9 +58,10 @@ export declare class PrefService extends HoistService {
|
|
|
58
58
|
*/
|
|
59
59
|
pushAsync(key: string, value: any): Promise<void>;
|
|
60
60
|
/**
|
|
61
|
-
* Push any pending buffered updates to persist newly set values to server.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* Push any pending buffered updates to persist newly set values to the server.
|
|
62
|
+
*
|
|
63
|
+
* Not typically called by applications. Called automatically by the framework after changes
|
|
64
|
+
* and when page is hidden/terminated.
|
|
64
65
|
*/
|
|
65
66
|
pushPendingAsync(): Promise<void>;
|
|
66
67
|
private pushPendingBuffered;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HoistService,
|
|
1
|
+
import { HoistService, Span, FullSpanConfig } from '@xh/hoist/core';
|
|
2
2
|
/**
|
|
3
3
|
* Client-side distributed tracing service for Hoist applications.
|
|
4
4
|
*
|
|
@@ -30,7 +30,7 @@ export declare class TraceService extends HoistService {
|
|
|
30
30
|
private conf;
|
|
31
31
|
/** Spans created before config available. */
|
|
32
32
|
private _preConfigSpans;
|
|
33
|
-
initAsync(
|
|
33
|
+
initAsync(): Promise<void>;
|
|
34
34
|
/** Is tracing currently enabled? */
|
|
35
35
|
get enabled(): boolean;
|
|
36
36
|
/**
|
|
@@ -64,12 +64,10 @@ export declare class TraceService extends HoistService {
|
|
|
64
64
|
*/
|
|
65
65
|
private exportSpan;
|
|
66
66
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
67
|
+
* Flush the queue of pending spans to the server.
|
|
68
|
+
* @internal - apps should generally allow this service to manage w/its internal debounce.
|
|
69
69
|
*/
|
|
70
70
|
pushPendingAsync(): Promise<void>;
|
|
71
|
-
/** Bound the pending buffer, silently dropping oldest spans (failed pushes are logged). */
|
|
72
|
-
private enforceCap;
|
|
73
71
|
/**
|
|
74
72
|
* Called by {@link ConfigService} once `xhTraceConfig` has loaded. Applies sampling
|
|
75
73
|
* decisions to all spans created during early startup and held in the pendingConfig
|
|
@@ -83,6 +81,10 @@ export declare class TraceService extends HoistService {
|
|
|
83
81
|
*/
|
|
84
82
|
noteConfigAvailable(): void;
|
|
85
83
|
private pushPendingBuffered;
|
|
84
|
+
/** Flush all pending spans to the server. */
|
|
85
|
+
private pushPendingInternalAsync;
|
|
86
|
+
/** Bound the pending buffer, silently dropping oldest spans (failed pushes are logged). */
|
|
87
|
+
private enforceCap;
|
|
86
88
|
/**
|
|
87
89
|
* Resolve a root-span sampling decision: a probabilistic decision from `sampleRules`. Rules
|
|
88
90
|
* match on tag keys; the reserved key `name` matches the span's name (glob-capable, same
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HoistService,
|
|
1
|
+
import { HoistService, TrackOptions } from '@xh/hoist/core';
|
|
2
2
|
/**
|
|
3
3
|
* Primary service for tracking any activity that an application's admins want to track.
|
|
4
4
|
* Activities are available for viewing/querying in the Admin Console's Client Activity tab.
|
|
@@ -9,14 +9,16 @@ export declare class TrackService extends HoistService {
|
|
|
9
9
|
static instance: TrackService;
|
|
10
10
|
private oncePerSessionSent;
|
|
11
11
|
private pending;
|
|
12
|
-
initAsync(
|
|
12
|
+
initAsync(): Promise<void>;
|
|
13
13
|
get conf(): ActivityTrackingConfig;
|
|
14
14
|
get enabled(): boolean;
|
|
15
15
|
/** Track User Activity. */
|
|
16
16
|
track(options: TrackOptions | string): void;
|
|
17
17
|
/**
|
|
18
18
|
* Flush the queue of pending activity tracking messages to the server.
|
|
19
|
-
*
|
|
19
|
+
*
|
|
20
|
+
* Not typically called by applications. Called automatically by the framework via a
|
|
21
|
+
* debounce and when the page is hidden/terminated.
|
|
20
22
|
*/
|
|
21
23
|
pushPendingAsync(): Promise<void>;
|
|
22
24
|
private pushPendingBuffered;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CallContextLike } from '@xh/hoist/core';
|
|
2
|
+
import type { FetchOptions } from '../FetchService';
|
|
3
|
+
/**
|
|
4
|
+
* Post a JSON body, using `fetch({keepalive: true})` when the page is no longer visible so the
|
|
5
|
+
* request can survive teardown (e.g. a flush as `XH.pageState` goes `hidden`/`frozen`/`terminated`).
|
|
6
|
+
*
|
|
7
|
+
* Keepalive bodies share a single browser-wide 64KB budget; if exceeded the request is never sent
|
|
8
|
+
* and we retry once uncapped (without keepalive), which still completes while the page is alive (the
|
|
9
|
+
* common `hidden` case). Real server errors are re-thrown, not re-posted.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare function terminationSafePostJson(opts: FetchOptions, ctx?: CallContextLike): Promise<any>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HoistProps } from '@xh/hoist/core';
|
|
1
|
+
import type { HoistProps } from '@xh/hoist/core';
|
|
2
2
|
/**
|
|
3
3
|
* HTML attribute name used to tag elements with stable test identifiers. Use as a computed
|
|
4
4
|
* property key on element specs - e.g. `{[TEST_ID]: 'my-grid'}` - to emit a `data-testid`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ResolvedLayoutProps, PlainObject } from '@xh/hoist/core';
|
|
1
|
+
import type { ResolvedLayoutProps, PlainObject } from '@xh/hoist/core';
|
|
2
2
|
/**
|
|
3
3
|
* These utils support accepting the CSS styles enumerated below as top-level props of a Component,
|
|
4
4
|
* and are typically accessed via the `@LayoutSupport` mixin (for class-based components) or the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "86.0.
|
|
3
|
+
"version": "86.0.1",
|
|
4
4
|
"description": "Hoist add-on for building and deploying React Applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
]
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@auth0/auth0-spa-js": "~2.
|
|
42
|
-
"@azure/msal-browser": "~5.
|
|
41
|
+
"@auth0/auth0-spa-js": "~2.21.1",
|
|
42
|
+
"@azure/msal-browser": "~5.13.0",
|
|
43
43
|
"@blueprintjs/core": "^6.3.2",
|
|
44
44
|
"@blueprintjs/datetime": "^6.0.6",
|
|
45
45
|
"@codemirror/commands": "~6.10.3",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"jwt-decode": "~4.0.0",
|
|
70
70
|
"lodash": "~4.18.0",
|
|
71
71
|
"lodash-inflection": "~1.5.0",
|
|
72
|
-
"mobx": "~6.
|
|
72
|
+
"mobx": "~6.16.1",
|
|
73
73
|
"mobx-react-lite": "~4.1.0",
|
|
74
74
|
"moment": "~2.30.1",
|
|
75
75
|
"numbro": "~2.5.0",
|
package/svc/PrefService.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {CallContextLike, HoistService, InitContext, XH} from '@xh/hoist/core';
|
|
|
8
8
|
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
9
9
|
import {debounced, deepFreeze, throwIf} from '@xh/hoist/utils/js';
|
|
10
10
|
import {cloneDeep, forEach, isEmpty, isEqual} from 'lodash';
|
|
11
|
+
import {terminationSafePostJson} from './impl/Fetch';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Service to read and set user-specific preference values.
|
|
@@ -34,7 +35,13 @@ export class PrefService extends HoistService {
|
|
|
34
35
|
private _updates = {};
|
|
35
36
|
|
|
36
37
|
override async initAsync(ctx: InitContext) {
|
|
37
|
-
|
|
38
|
+
// Flush on page teardown while the page is still alive.
|
|
39
|
+
this.addReaction({
|
|
40
|
+
track: () => XH.pageState,
|
|
41
|
+
run: () => {
|
|
42
|
+
if (!XH.pageIsVisible) this.pushPendingAsync();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
38
45
|
return this.loadPrefsAsync(ctx);
|
|
39
46
|
}
|
|
40
47
|
|
|
@@ -112,27 +119,30 @@ export class PrefService extends HoistService {
|
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
/**
|
|
115
|
-
* Push any pending buffered updates to persist newly set values to server.
|
|
116
|
-
*
|
|
117
|
-
*
|
|
122
|
+
* Push any pending buffered updates to persist newly set values to the server.
|
|
123
|
+
*
|
|
124
|
+
* Not typically called by applications. Called automatically by the framework after changes
|
|
125
|
+
* and when page is hidden/terminated.
|
|
118
126
|
*/
|
|
119
127
|
async pushPendingAsync() {
|
|
120
128
|
const updates = this._updates;
|
|
121
129
|
if (isEmpty(updates)) return;
|
|
122
130
|
|
|
131
|
+
// Clear synchronously with the capture, so overlapping flushes cannot post twice.
|
|
132
|
+
this._updates = {};
|
|
133
|
+
|
|
123
134
|
await this.runner()
|
|
124
135
|
.span('set')
|
|
125
|
-
.run(
|
|
126
|
-
|
|
127
|
-
await XH.postJson(
|
|
136
|
+
.run(ctx =>
|
|
137
|
+
terminationSafePostJson(
|
|
128
138
|
{
|
|
129
139
|
url: 'xh/setPrefs',
|
|
130
140
|
body: updates,
|
|
131
141
|
params: {clientUsername: XH.getUsername()}
|
|
132
142
|
},
|
|
133
143
|
ctx
|
|
134
|
-
)
|
|
135
|
-
|
|
144
|
+
)
|
|
145
|
+
);
|
|
136
146
|
}
|
|
137
147
|
|
|
138
148
|
//-------------------
|
|
@@ -140,7 +150,7 @@ export class PrefService extends HoistService {
|
|
|
140
150
|
//-------------------
|
|
141
151
|
@debounced(5 * SECONDS)
|
|
142
152
|
private pushPendingBuffered() {
|
|
143
|
-
this.pushPendingAsync();
|
|
153
|
+
void this.pushPendingAsync();
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
private async loadPrefsAsync(ctx: CallContextLike) {
|
package/svc/TraceService.ts
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {HoistService,
|
|
7
|
+
import {HoistService, PlainObject, XH, Span, FullSpanConfig} from '@xh/hoist/core';
|
|
8
8
|
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
9
9
|
import {debounced, parseNameSource} from '@xh/hoist/utils/js';
|
|
10
|
-
import {every, forEach, groupBy, isEmpty, isString, omitBy} from 'lodash';
|
|
10
|
+
import {every, forEach, groupBy, isEmpty, isString, omitBy, takeRight} from 'lodash';
|
|
11
|
+
import {terminationSafePostJson} from './impl/Fetch';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Client-side distributed tracing service for Hoist applications.
|
|
@@ -48,8 +49,14 @@ export class TraceService extends HoistService {
|
|
|
48
49
|
//------------------
|
|
49
50
|
// Initialization
|
|
50
51
|
//------------------
|
|
51
|
-
override async initAsync(
|
|
52
|
-
|
|
52
|
+
override async initAsync() {
|
|
53
|
+
// Flush on page teardown while the page is still alive.
|
|
54
|
+
this.addReaction({
|
|
55
|
+
track: () => XH.pageState,
|
|
56
|
+
run: () => {
|
|
57
|
+
if (!XH.pageIsVisible) this.pushPendingInternalAsync();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
//------------------
|
|
@@ -159,44 +166,11 @@ export class TraceService extends HoistService {
|
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
169
|
+
* Flush the queue of pending spans to the server.
|
|
170
|
+
* @internal - apps should generally allow this service to manage w/its internal debounce.
|
|
164
171
|
*/
|
|
165
172
|
async pushPendingAsync() {
|
|
166
|
-
|
|
167
|
-
if (isEmpty(spans)) return;
|
|
168
|
-
|
|
169
|
-
this._pending = [];
|
|
170
|
-
try {
|
|
171
|
-
await XH.postJson({
|
|
172
|
-
url: 'xh/submitSpans',
|
|
173
|
-
body: spans.map(s => s.toJSON()),
|
|
174
|
-
params: {
|
|
175
|
-
clientUsername: XH.getUsername()
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
} catch (e) {
|
|
179
|
-
if (isRetryableError(e)) {
|
|
180
|
-
// Transient failure - re-queue the batch (ahead of newer spans) to retry on the
|
|
181
|
-
// next flush, then bound the buffer in case the outage is prolonged.
|
|
182
|
-
this._pending = [...spans, ...this._pending];
|
|
183
|
-
this.enforceCap();
|
|
184
|
-
this.logError('Failed to push spans - will retry on next flush', e);
|
|
185
|
-
} else {
|
|
186
|
-
// Permanent (client-side) rejection - drop the batch so it can't deadlock the
|
|
187
|
-
// pipe (e.g. a session mismatch or oversized payload would fail forever).
|
|
188
|
-
this.logError('Server rejected span batch - dropping', e);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Bound the pending buffer, silently dropping oldest spans (failed pushes are logged). */
|
|
194
|
-
private enforceCap() {
|
|
195
|
-
const {_pending} = this,
|
|
196
|
-
{MAX_PENDING} = TraceService;
|
|
197
|
-
if (_pending.length > MAX_PENDING) {
|
|
198
|
-
_pending.splice(0, _pending.length - MAX_PENDING);
|
|
199
|
-
}
|
|
173
|
+
return this.pushPendingInternalAsync();
|
|
200
174
|
}
|
|
201
175
|
|
|
202
176
|
/**
|
|
@@ -237,7 +211,45 @@ export class TraceService extends HoistService {
|
|
|
237
211
|
|
|
238
212
|
@debounced(5 * SECONDS)
|
|
239
213
|
private pushPendingBuffered() {
|
|
240
|
-
void this.
|
|
214
|
+
void this.pushPendingInternalAsync();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Flush all pending spans to the server. */
|
|
218
|
+
private async pushPendingInternalAsync() {
|
|
219
|
+
const spans = this._pending;
|
|
220
|
+
if (isEmpty(spans)) return;
|
|
221
|
+
|
|
222
|
+
// Clear synchronously with the capture, so overlapping flushes cannot post twice.
|
|
223
|
+
this._pending = [];
|
|
224
|
+
try {
|
|
225
|
+
await terminationSafePostJson({
|
|
226
|
+
url: 'xh/submitSpans',
|
|
227
|
+
body: spans.map(s => s.toJSON()),
|
|
228
|
+
params: {
|
|
229
|
+
clientUsername: XH.getUsername()
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
} catch (e) {
|
|
233
|
+
if (isRetryableError(e)) {
|
|
234
|
+
// Transient failure - re-queue the batch (ahead of newer spans) to retry on the
|
|
235
|
+
// next flush, then bound the buffer in case the outage is prolonged.
|
|
236
|
+
this._pending = [...spans, ...this._pending];
|
|
237
|
+
this.enforceCap();
|
|
238
|
+
this.logError('Failed to push spans - will retry on next flush', e);
|
|
239
|
+
} else {
|
|
240
|
+
// Permanent (client-side) rejection - drop the batch so it can't deadlock the
|
|
241
|
+
// pipe (e.g. a session mismatch or oversized payload would fail forever).
|
|
242
|
+
this.logError('Server rejected span batch - dropping', e);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Bound the pending buffer, silently dropping oldest spans (failed pushes are logged). */
|
|
248
|
+
private enforceCap() {
|
|
249
|
+
const {MAX_PENDING} = TraceService;
|
|
250
|
+
if (this._pending.length > MAX_PENDING) {
|
|
251
|
+
this._pending = takeRight(this._pending, MAX_PENDING);
|
|
252
|
+
}
|
|
241
253
|
}
|
|
242
254
|
|
|
243
255
|
/**
|
package/svc/TrackService.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {HoistService,
|
|
7
|
+
import {HoistService, PlainObject, TrackOptions, XH} from '@xh/hoist/core';
|
|
8
8
|
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
9
9
|
import {isOmitted} from '@xh/hoist/utils/impl';
|
|
10
10
|
import {debounced, stripTags, withDefault} from '@xh/hoist/utils/js';
|
|
11
11
|
import {isEmpty, isNil, isString} from 'lodash';
|
|
12
|
+
import {terminationSafePostJson} from './impl/Fetch';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Primary service for tracking any activity that an application's admins want to track.
|
|
@@ -23,8 +24,14 @@ export class TrackService extends HoistService {
|
|
|
23
24
|
private oncePerSessionSent = new Map();
|
|
24
25
|
private pending: PlainObject[] = [];
|
|
25
26
|
|
|
26
|
-
override async initAsync(
|
|
27
|
-
|
|
27
|
+
override async initAsync() {
|
|
28
|
+
// Flush on page teardown while the page is still alive
|
|
29
|
+
this.addReaction({
|
|
30
|
+
track: () => XH.pageState,
|
|
31
|
+
run: () => {
|
|
32
|
+
if (!XH.pageIsVisible) this.pushPendingAsync();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
get conf(): ActivityTrackingConfig {
|
|
@@ -94,25 +101,29 @@ export class TrackService extends HoistService {
|
|
|
94
101
|
|
|
95
102
|
/**
|
|
96
103
|
* Flush the queue of pending activity tracking messages to the server.
|
|
97
|
-
*
|
|
104
|
+
*
|
|
105
|
+
* Not typically called by applications. Called automatically by the framework via a
|
|
106
|
+
* debounce and when the page is hidden/terminated.
|
|
98
107
|
*/
|
|
99
108
|
async pushPendingAsync() {
|
|
100
109
|
const {pending} = this;
|
|
101
110
|
if (isEmpty(pending)) return;
|
|
102
111
|
|
|
112
|
+
// Clear synchronously with the capture, so overlapping flushes cannot post twice.
|
|
113
|
+
this.pending = [];
|
|
114
|
+
|
|
103
115
|
await this.runner()
|
|
104
116
|
.span('push')
|
|
105
|
-
.run(
|
|
106
|
-
|
|
107
|
-
await XH.postJson(
|
|
117
|
+
.run(ctx =>
|
|
118
|
+
terminationSafePostJson(
|
|
108
119
|
{
|
|
109
120
|
url: 'xh/track',
|
|
110
121
|
body: {entries: pending},
|
|
111
122
|
params: {clientUsername: XH.getUsername()}
|
|
112
123
|
},
|
|
113
124
|
ctx
|
|
114
|
-
)
|
|
115
|
-
|
|
125
|
+
)
|
|
126
|
+
);
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
//------------------
|
|
@@ -120,7 +131,7 @@ export class TrackService extends HoistService {
|
|
|
120
131
|
//------------------
|
|
121
132
|
@debounced(10 * SECONDS)
|
|
122
133
|
private pushPendingBuffered() {
|
|
123
|
-
this.pushPendingAsync();
|
|
134
|
+
void this.pushPendingAsync();
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
private toServerJson(options: TrackOptions): PlainObject {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file belongs to Hoist, an application development toolkit
|
|
3
|
+
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
|
|
4
|
+
*
|
|
5
|
+
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
import {CallContextLike, XH} from '@xh/hoist/core';
|
|
8
|
+
import type {FetchOptions} from '../FetchService';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Post a JSON body, using `fetch({keepalive: true})` when the page is no longer visible so the
|
|
12
|
+
* request can survive teardown (e.g. a flush as `XH.pageState` goes `hidden`/`frozen`/`terminated`).
|
|
13
|
+
*
|
|
14
|
+
* Keepalive bodies share a single browser-wide 64KB budget; if exceeded the request is never sent
|
|
15
|
+
* and we retry once uncapped (without keepalive), which still completes while the page is alive (the
|
|
16
|
+
* common `hidden` case). Real server errors are re-thrown, not re-posted.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export async function terminationSafePostJson(
|
|
21
|
+
opts: FetchOptions,
|
|
22
|
+
ctx?: CallContextLike
|
|
23
|
+
): Promise<any> {
|
|
24
|
+
if (XH.pageIsVisible) return XH.postJson(opts, ctx);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
return await XH.postJson({...opts, fetchOpts: {...opts.fetchOpts, keepalive: true}}, ctx);
|
|
28
|
+
} catch (e: any) {
|
|
29
|
+
// Retry uncapped if keepalive was never sent (over-budget); re-throw real server errors.
|
|
30
|
+
if (e.isServerUnavailable) return XH.postJson(opts, ctx);
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {Exception} from '@xh/hoist/exception';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Copy the given text to the system clipboard.
|
|
@@ -53,5 +53,5 @@ function copyViaExecCommand(text: string): void {
|
|
|
53
53
|
selection.removeAllRanges();
|
|
54
54
|
document.body.removeChild(span);
|
|
55
55
|
}
|
|
56
|
-
if (!success) throw
|
|
56
|
+
if (!success) throw Exception.create('Clipboard copy not allowed');
|
|
57
57
|
}
|
package/utils/js/TestUtils.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {LayoutProps, ResolvedLayoutProps, PlainObject} from '@xh/hoist/core';
|
|
7
|
+
import type {LayoutProps, ResolvedLayoutProps, PlainObject} from '@xh/hoist/core';
|
|
8
8
|
import {forOwn, isEmpty, isNumber, isString, isNil, omit, pick} from 'lodash';
|
|
9
9
|
|
|
10
10
|
const XH_PAD_VAR = 'var(--xh-pad-px)';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {Content} from '@xh/hoist/core';
|
|
7
|
+
import type {Content} from '@xh/hoist/core';
|
|
8
8
|
import {ReactElement, cloneElement, createElement, isValidElement} from 'react';
|
|
9
9
|
import {isFunction, isNil} from 'lodash';
|
|
10
10
|
import {renderToStaticMarkup as reactRenderToStaticMarkup} from 'react-dom/server';
|