-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathLoadSupport.ts
More file actions
175 lines (151 loc) · 6.14 KB
/
Copy pathLoadSupport.ts
File metadata and controls
175 lines (151 loc) · 6.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/*
* This file belongs to Hoist, an application development toolkit
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
*
* Copyright © 2026 Extremely Heavy Industries Inc.
*/
import {
CallContextLike,
HoistBase,
LoadSpecConfig,
managed,
PlainObject,
RefreshContextModel,
TaskObserver
} from '../';
import {LoadSpec, Loadable} from './';
import {makeObservable, observable, runInAction} from '@xh/hoist/mobx';
import {logDebug, logError} from '@xh/hoist/utils/js';
import {pull} from 'lodash';
/**
* Provides support for objects that participate in Hoist's loading/refresh lifecycle.
*
* This utility is used by core Hoist classes such as {@link HoistModel} and {@link HoistService}.
* Model and service instances will automatically create an instance of this class if they have
* declared a concrete implementation of `doLoadAsync()`, signalling that they wish to take
* advantage of the additional tracking and management provided here.
*
* Not typically created directly by applications.
*/
export class LoadSupport extends HoistBase implements Loadable {
lastRequested: LoadSpec = null;
lastSucceeded: LoadSpec = null;
@managed
loadObserver: TaskObserver = TaskObserver.trackLast();
@observable.ref
lastLoadRequested: Date = null;
@observable.ref
lastLoadCompleted: Date = null;
@observable.ref
lastLoadException: any = null;
target: Loadable;
constructor(target: Loadable) {
super();
makeObservable(this);
this.target = target;
}
/**
* Trigger a managed load through the target's {@link doLoadAsync} template method. Use this
* (or {@link refreshAsync}/{@link autoRefreshAsync}) - do not call `doLoadAsync` directly,
* so that Hoist creates a fresh {@link LoadSpec} and tracks the request.
*
* Accepts an optional config to set `isRefresh`/`isAutoRefresh` flags or app-specific `meta`.
*
* See the lifecycle doc (`docs/lifecycle-models-and-services.md#loading-doloadasync`) for the
* full load/refresh lifecycle.
*/
async loadAsync(loadSpec?: LoadSpecConfig | CallContextLike) {
// Guard against clearly-invalid input - e.g. loadAsync wired directly as a reaction
// handler, which would pass the reaction's tracked value (often a primitive) as this arg.
// Log rather than throw, then proceed with a default spec.
if (loadSpec != null && (typeof loadSpec !== 'object' || Array.isArray(loadSpec))) {
this.logError(
'Invalid argument passed to loadAsync() - ignoring. If triggered via a reaction, ' +
'ensure the call is wrapped in a closure.',
loadSpec
);
loadSpec = null;
}
// Favor any concrete loadSpec from a call context (a CallContext forwarded from an
// upstream caller is a common case here).
const config: LoadSpecConfig = loadSpec?.['loadSpec'] ?? loadSpec,
newSpec = new LoadSpec(config ?? {}, this);
return this.doLoadAsync(newSpec);
}
async refreshAsync(meta?: PlainObject) {
return this.loadAsync({meta, isRefresh: true});
}
async autoRefreshAsync(meta?: PlainObject) {
return this.loadAsync({meta, isAutoRefresh: true});
}
/**
* Run the managed-load lifecycle for the target: short-circuits redundant
* auto-refreshes, links the load to the `loadObserver`, delegates to
* `target.doLoadAsync(loadSpec)`, and updates `lastLoadCompleted` /
* `lastLoadException` on completion.
*
* Application code should not override or call this directly - it is the
* orchestrator that the public entry points (`loadAsync`/`refreshAsync`/
* `autoRefreshAsync`) ultimately invoke. Application templates that opt
* into managed loading override `doLoadAsync` on their own model/service
* class instead (see {@link Loadable.doLoadAsync}).
*/
async doLoadAsync(loadSpec: LoadSpec) {
let {target, loadObserver} = this;
// Auto-refresh:
// Skip if we have a pending triggered refresh, and never link to loadObserver
if (loadSpec.isAutoRefresh) {
if (loadObserver.isPending) return;
loadObserver = null;
}
runInAction(() => (this.lastLoadRequested = new Date()));
this.lastRequested = loadSpec;
let exception = null;
return target
.doLoadAsync(loadSpec)
.linkTo(loadObserver)
.catch(e => {
exception = e;
throw e;
})
.finally(() => {
runInAction(() => {
this.lastLoadCompleted = new Date();
this.lastLoadException = exception;
});
if (!exception) {
this.lastSucceeded = loadSpec;
}
if (target instanceof RefreshContextModel) return;
const elapsed = this.lastLoadCompleted.getTime() - this.lastLoadRequested.getTime(),
status = exception ? 'failed' : null,
msg = pull([loadSpec.typeDisplay, status, `${elapsed}ms`, exception], null);
if (exception) {
if (exception.isRoutine) {
logDebug(msg, target);
} else {
logError(msg, target);
}
} else {
logDebug(msg, target);
}
});
}
}
/**
* Load a collection of objects concurrently.
*
* Note that this method uses 'allSettled' in its implementation, meaning a failure of any one call
* will not cause the entire batch to throw.
*
* @param objs - list of objects to be loaded
* @param loadSpec - optional metadata related to this request.
*/
export async function loadAllAsync(objs: Loadable[], loadSpec?: LoadSpec | any) {
const promises = objs.map(it => it.loadAsync(loadSpec)),
ret = await Promise.allSettled(promises);
ret.filter(it => it.status === 'rejected').forEach((err: any) =>
logError(['Failed to Load Object', err.reason])
);
return ret;
}