@komaci/prefetch 240.1.3
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/build/__mocks__/@lwrjs/router.d.ts +5 -0
- package/build/__mocks__/lwr/router.d.ts +5 -0
- package/build/__mocks__/mockAdgModule.d.ts +3 -0
- package/build/__mocks__/o11y/client.d.ts +3 -0
- package/build/__mocks__/o11y_schema/sf_komaci.d.ts +5 -0
- package/build/modules/komaci/prefetch/prefetch.d.ts +5 -0
- package/build/modules/komaci/prefetch/prefetch.js +53 -0
- package/build/modules/komaci/prefetchService/prefetchService.d.ts +114 -0
- package/build/modules/komaci/prefetchService/prefetchService.js +509 -0
- package/build/modules/komaci/prefetchTypes/prefetchTypes.d.ts +85 -0
- package/build/modules/komaci/prefetchTypes/prefetchTypes.js +30 -0
- package/package.json +39 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Router, PageReference, RouteDefinition } from 'lwr/router';
|
|
2
|
+
import type { IPrefetchService, PrefetchConfig, AddressStatus } from 'komaci/prefetchTypes';
|
|
3
|
+
export declare function prefetch(routes?: RouteDefinition[], pageReferences?: PageReference[], config?: PrefetchConfig): Promise<AddressStatus[]>;
|
|
4
|
+
export declare function getPrefetchService<TAddress = PageReference>(router: Router<TAddress>, addresses: TAddress[], config?: PrefetchConfig<TAddress>): IPrefetchService;
|
|
5
|
+
//# sourceMappingURL=prefetch.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { PrefetchService } from 'komaci/prefetchService';
|
|
3
|
+
import { createRouter } from 'lwr/router';
|
|
4
|
+
// NOTE: Backward compatible prefetch until primer is updated to handle this.
|
|
5
|
+
// TODO W-9690128 - Let's potentially remove this prefetch method and update lsdk code to call `getPrefetchState` directly.
|
|
6
|
+
export function prefetch(routes = [], pageReferences = [], config) {
|
|
7
|
+
if (!routes?.length || !pageReferences?.length) {
|
|
8
|
+
return Promise.resolve([]);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const router = createRouter({
|
|
12
|
+
routes
|
|
13
|
+
});
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
let currentState;
|
|
16
|
+
const addressesProcessed = [];
|
|
17
|
+
const wrappedConfig = {
|
|
18
|
+
maxConcurrent: config?.maxConcurrent,
|
|
19
|
+
onAddressProcessed: status => {
|
|
20
|
+
addressesProcessed.push(status);
|
|
21
|
+
if (config && config.onAddressProcessed) config.onAddressProcessed(status);
|
|
22
|
+
},
|
|
23
|
+
onStateChanged: status => {
|
|
24
|
+
const {
|
|
25
|
+
state
|
|
26
|
+
} = status;
|
|
27
|
+
|
|
28
|
+
if (state === 'running') {
|
|
29
|
+
if (!currentState) {
|
|
30
|
+
currentState = state;
|
|
31
|
+
} else {
|
|
32
|
+
reject(new Error(`Unexpected state transition. ${currentState} -> ${state}`));
|
|
33
|
+
}
|
|
34
|
+
} else if (state === 'done') {
|
|
35
|
+
if (addressesProcessed.length !== pageReferences.length) {
|
|
36
|
+
reject(new Error(`Prefetch error, expected ${pageReferences.length} addresses processed, but only got ${addressesProcessed.length}`));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
resolve(addressesProcessed);
|
|
40
|
+
} else if (state === 'error') {
|
|
41
|
+
reject(new Error(`Prefetch error: ${status.message}`));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (config && config.onStateChanged) config.onStateChanged(status);
|
|
45
|
+
},
|
|
46
|
+
instrumentationContext: config?.instrumentationContext
|
|
47
|
+
};
|
|
48
|
+
getPrefetchService(router, pageReferences, wrappedConfig);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function getPrefetchService(router, addresses, config) {
|
|
52
|
+
return new PrefetchService(router, addresses, config);
|
|
53
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Router, PageReference } from 'lwr/router';
|
|
2
|
+
import { PrefetchState } from 'komaci/prefetchTypes';
|
|
3
|
+
import type { IPrefetchService, PrefetchConfig, AdgRoutingResult, AdgResolutionGroup } from 'komaci/prefetchTypes';
|
|
4
|
+
import type { BulkResolutionStatus } from '@komaci/resolver';
|
|
5
|
+
export declare class PrefetchService<TAddress = PageReference> implements IPrefetchService {
|
|
6
|
+
private readonly config;
|
|
7
|
+
private router;
|
|
8
|
+
private adgMap;
|
|
9
|
+
private resolutionGroups;
|
|
10
|
+
private state;
|
|
11
|
+
private apiOptions;
|
|
12
|
+
private _instrumentation;
|
|
13
|
+
private _activity;
|
|
14
|
+
private readonly _startTime;
|
|
15
|
+
private readonly _size;
|
|
16
|
+
constructor(router: Router<TAddress>, addresses: TAddress[], config?: PrefetchConfig<TAddress>);
|
|
17
|
+
/**
|
|
18
|
+
* Processes the given address through the router, retrieving the AdgFunction if
|
|
19
|
+
* available.
|
|
20
|
+
* @param address to process
|
|
21
|
+
* @returns AdgRoutingResult encapsulating the status, original address, and adgFn
|
|
22
|
+
*/
|
|
23
|
+
private processAddress;
|
|
24
|
+
private getErrorAddress;
|
|
25
|
+
/**
|
|
26
|
+
* Determine if the router has an adg for the given address. For each result
|
|
27
|
+
* with an adg, group the address with other addresses that resolve to the same
|
|
28
|
+
* adg.
|
|
29
|
+
*
|
|
30
|
+
* @param address that resolves to this routingResult
|
|
31
|
+
* @param routingResult provided by the router for this address
|
|
32
|
+
* @returns adgRoutingResult
|
|
33
|
+
*/
|
|
34
|
+
private onRoutingResult;
|
|
35
|
+
/**
|
|
36
|
+
* Updates the adgMap with the given adgFn, creating the entry if needed
|
|
37
|
+
* and updating the pageReference array for the given address.
|
|
38
|
+
*
|
|
39
|
+
* @param adgModule adgModule that will act as a key in the map
|
|
40
|
+
* @param address address associated with this adgFn
|
|
41
|
+
*/
|
|
42
|
+
private updateAdgMap;
|
|
43
|
+
/**
|
|
44
|
+
* Initiates the prefetch process for the given array of addresses.
|
|
45
|
+
* - Addresses are ran through the router to resolve their associated AdgModule, if any
|
|
46
|
+
* - Addresses that resolve to AdgModule are grouped together in order to be resolved in a batch
|
|
47
|
+
* - Addresses that do no resolve to an AdgModule are rejected
|
|
48
|
+
*
|
|
49
|
+
* @param addresses
|
|
50
|
+
*/
|
|
51
|
+
private prefetch;
|
|
52
|
+
/**
|
|
53
|
+
* Processes a list of addresses through the router in series
|
|
54
|
+
* @param addresses to process
|
|
55
|
+
* @returns { AdgRoutingResult[] } a list of AdgRoutingResult
|
|
56
|
+
*/
|
|
57
|
+
processAllAddresses(addresses: TAddress[]): Promise<AdgRoutingResult<TAddress>[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Split all the addresses based on the maxConcurrent configuration
|
|
60
|
+
* bulkResolve them in iterations
|
|
61
|
+
*/
|
|
62
|
+
private bulkResolveGuard;
|
|
63
|
+
/**
|
|
64
|
+
* Resolves the given adgFunction with the given resolutionGroup
|
|
65
|
+
* @param adgFn to resolve for the inputs provided in the resolutionContext
|
|
66
|
+
* @param addresses the list of addresses to be placed into a new BulkResolver
|
|
67
|
+
*/
|
|
68
|
+
private bulkResolve;
|
|
69
|
+
/**
|
|
70
|
+
* StatusCallback to BulkResolver, invoked when resolver emits a new status.
|
|
71
|
+
* Updates the prefetchService's status if applicable.
|
|
72
|
+
* @param bulkResolutionGroup
|
|
73
|
+
* @param action InstrumentationContext
|
|
74
|
+
* @param status
|
|
75
|
+
*/
|
|
76
|
+
private onBulkResolutionStatus;
|
|
77
|
+
/**
|
|
78
|
+
* Stops all bulkResolvers
|
|
79
|
+
*/
|
|
80
|
+
stop(): void;
|
|
81
|
+
/**
|
|
82
|
+
* Called upon unexpected conditions that may indicate to the consumer that
|
|
83
|
+
* the prefetchService or its associated resolver is in an invalid state
|
|
84
|
+
* @param message
|
|
85
|
+
*/
|
|
86
|
+
private onUnexpectedCondition;
|
|
87
|
+
/**
|
|
88
|
+
* Called when resolution for a given address has completed, or was otherwise
|
|
89
|
+
* rejected.
|
|
90
|
+
* @param address that is done
|
|
91
|
+
* @param state completed when resolved, or rejected when either no ADG exists, or resolver emitted an error with the given address
|
|
92
|
+
* @param reasons reasons for rejected status, if any
|
|
93
|
+
*/
|
|
94
|
+
private onAddressDone;
|
|
95
|
+
/**
|
|
96
|
+
* Updates the current PrefetchState to done if every bulkResolver has emitted
|
|
97
|
+
* a non-running status for all inputs.
|
|
98
|
+
*/
|
|
99
|
+
updateStateIfAllDone(bulkResolutionContext: AdgResolutionGroup<TAddress>, status: BulkResolutionStatus): void;
|
|
100
|
+
/**
|
|
101
|
+
* Called when resolver emits a "sink" state for an address or all addresses
|
|
102
|
+
* @param status
|
|
103
|
+
* @param bulkResolutionContext
|
|
104
|
+
*/
|
|
105
|
+
private onResolutionDone;
|
|
106
|
+
/**
|
|
107
|
+
* Updates PrefetchState and notifies consumer via config.onStateChanged callback
|
|
108
|
+
* @param newState the new state
|
|
109
|
+
* @param [message] state message, if any
|
|
110
|
+
*/
|
|
111
|
+
private updateState;
|
|
112
|
+
getState(): PrefetchState;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=prefetchService.d.ts.map
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import { ERROR_PREFIX, PrefetchStatusStates as STATE } from 'komaci/prefetchTypes';
|
|
2
|
+
import { getInstrumentation } from 'o11y/client';
|
|
3
|
+
import { getBulkAdgResolver } from 'komaci/resolver';
|
|
4
|
+
import ResolverConfig from 'komaci/resolverConfig';
|
|
5
|
+
import { InstrumentedAction } from 'komaci/instrumentedAction';
|
|
6
|
+
import { bulkResolveSchema, prefetchSchema, prefetchServiceSchema, totalRoutingSchema } from 'o11y_schema/sf_komaci';
|
|
7
|
+
const NO_ADG_MODULE_FOUND = 'No ADG module found for given address.';
|
|
8
|
+
const ValidStatusTransitions = {
|
|
9
|
+
[STATE.init]: [STATE.running, STATE.stopped, STATE.error],
|
|
10
|
+
[STATE.running]: [STATE.done, STATE.error, STATE.stopped],
|
|
11
|
+
[STATE.error]: [STATE.stopped, STATE.done],
|
|
12
|
+
[STATE.done]: [STATE.stopped],
|
|
13
|
+
[STATE.stopped]: [],
|
|
14
|
+
|
|
15
|
+
isValid(oldState, newState) {
|
|
16
|
+
return this[oldState].includes(newState);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
};
|
|
20
|
+
export class PrefetchService {
|
|
21
|
+
config = {};
|
|
22
|
+
state = STATE.init;
|
|
23
|
+
apiOptions = {};
|
|
24
|
+
|
|
25
|
+
constructor(router, addresses, config = {}) {
|
|
26
|
+
if (!router) {
|
|
27
|
+
throw new TypeError(`${ERROR_PREFIX} Must provide Router.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.config = config;
|
|
31
|
+
this.apiOptions.instrumentationContext = this.config.instrumentationContext ? this.config.instrumentationContext : undefined;
|
|
32
|
+
this.router = router;
|
|
33
|
+
this.adgMap = new Map();
|
|
34
|
+
this.resolutionGroups = [];
|
|
35
|
+
this._instrumentation = getInstrumentation('komaci');
|
|
36
|
+
this._activity = this._instrumentation.startActivity('prefetch', this.apiOptions);
|
|
37
|
+
this._size = addresses.length; //FIXME: to use o11y.startActivity() when o11y available
|
|
38
|
+
|
|
39
|
+
this._startTime = Date.now(); // setting Instrumentation into ResolverConfig singleton so that the Resolver can
|
|
40
|
+
// access the rootActivityId to enable multiple root activity instrumentation
|
|
41
|
+
|
|
42
|
+
if (this.config?.instrumentationContext && ResolverConfig) {
|
|
43
|
+
ResolverConfig.InstrumentationContext = this.config.instrumentationContext;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this.config?.downloadImage && ResolverConfig) {
|
|
47
|
+
ResolverConfig.DownloadImage = this.config.downloadImage;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.prefetch(addresses);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Processes the given address through the router, retrieving the AdgFunction if
|
|
54
|
+
* available.
|
|
55
|
+
* @param address to process
|
|
56
|
+
* @returns AdgRoutingResult encapsulating the status, original address, and adgFn
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
processAddress(address) {
|
|
61
|
+
const userSchemaData = {
|
|
62
|
+
page: JSON.stringify(address)
|
|
63
|
+
};
|
|
64
|
+
let resolveViewResult;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
resolveViewResult = this.router.resolveView(address);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
this._instrumentation.error(err, prefetchSchema, userSchemaData, this.apiOptions);
|
|
70
|
+
|
|
71
|
+
return Promise.resolve(this.getErrorAddress(address, 'Unexpected exception during routing: ' + err?.message));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return resolveViewResult.then(destination => {
|
|
75
|
+
return this.onRoutingResult(address, destination);
|
|
76
|
+
}).then(value => {
|
|
77
|
+
this._instrumentation.log(prefetchSchema, userSchemaData, this.apiOptions);
|
|
78
|
+
|
|
79
|
+
return value;
|
|
80
|
+
}).catch(err => {
|
|
81
|
+
this._instrumentation.error(err, prefetchSchema, userSchemaData, this.apiOptions);
|
|
82
|
+
|
|
83
|
+
return this.getErrorAddress(address, err?.message ? 'Unexpected exception processing routing result: ' + err?.message : 'Unknown exception processing routing result.');
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getErrorAddress(address, reason) {
|
|
88
|
+
return {
|
|
89
|
+
address,
|
|
90
|
+
success: false,
|
|
91
|
+
reason: reason
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Determine if the router has an adg for the given address. For each result
|
|
96
|
+
* with an adg, group the address with other addresses that resolve to the same
|
|
97
|
+
* adg.
|
|
98
|
+
*
|
|
99
|
+
* @param address that resolves to this routingResult
|
|
100
|
+
* @param routingResult provided by the router for this address
|
|
101
|
+
* @returns adgRoutingResult
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async onRoutingResult(address, routingResult) {
|
|
106
|
+
const adg = 'komaci';
|
|
107
|
+
const {
|
|
108
|
+
viewset = {}
|
|
109
|
+
} = routingResult;
|
|
110
|
+
let adgModuleImporter; // if routing handler only provides komaci view, use komaci module importer
|
|
111
|
+
|
|
112
|
+
if (viewset[adg]) {
|
|
113
|
+
adgModuleImporter = viewset[adg];
|
|
114
|
+
} else if (viewset['default']) {
|
|
115
|
+
// calculate komaci specifier using generic default viewset
|
|
116
|
+
const viewInfo = viewset['default'];
|
|
117
|
+
|
|
118
|
+
if (viewInfo) {
|
|
119
|
+
if (viewInfo.specifier) {
|
|
120
|
+
const komaciSpecifier = '@salesforce/komaci/' + viewInfo.specifier.replace('/', '__'); // create adgModuleImporter using komaciSpecifier
|
|
121
|
+
|
|
122
|
+
adgModuleImporter = () => import(komaciSpecifier);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const adgModuleImport = await adgModuleImporter?.();
|
|
128
|
+
const adgRoutingResult = {
|
|
129
|
+
address,
|
|
130
|
+
success: false // assume the worst
|
|
131
|
+
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (adgModuleImport?.default) {
|
|
135
|
+
const adgModule = adgModuleImport.default;
|
|
136
|
+
adgRoutingResult.success = true;
|
|
137
|
+
adgRoutingResult.adgModule = adgModule;
|
|
138
|
+
this.updateAdgMap(adgModule, address);
|
|
139
|
+
} else {
|
|
140
|
+
adgRoutingResult.reason = NO_ADG_MODULE_FOUND;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return adgRoutingResult;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Updates the adgMap with the given adgFn, creating the entry if needed
|
|
147
|
+
* and updating the pageReference array for the given address.
|
|
148
|
+
*
|
|
149
|
+
* @param adgModule adgModule that will act as a key in the map
|
|
150
|
+
* @param address address associated with this adgFn
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
updateAdgMap(adgModule, address) {
|
|
155
|
+
const addresses = this.adgMap.get(adgModule); // check to see if adg has already been added, and append address to the list
|
|
156
|
+
|
|
157
|
+
if (addresses) {
|
|
158
|
+
// if highVolumePriming is enabled, we only wish to resolve each unique module once, so don't add additional addresses to the list for resolution
|
|
159
|
+
if (!this.config.highVolumePriming) {
|
|
160
|
+
addresses.push(address);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
// adg hasn't been added, create entry.
|
|
164
|
+
this.adgMap.set(adgModule, [address]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Initiates the prefetch process for the given array of addresses.
|
|
169
|
+
* - Addresses are ran through the router to resolve their associated AdgModule, if any
|
|
170
|
+
* - Addresses that resolve to AdgModule are grouped together in order to be resolved in a batch
|
|
171
|
+
* - Addresses that do no resolve to an AdgModule are rejected
|
|
172
|
+
*
|
|
173
|
+
* @param addresses
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
prefetch(addresses) {
|
|
178
|
+
this.updateState(STATE.running);
|
|
179
|
+
this.processAllAddresses(addresses).then(adgRoutingResults => {
|
|
180
|
+
const rejected = adgRoutingResults.filter(({
|
|
181
|
+
success
|
|
182
|
+
}) => !success);
|
|
183
|
+
rejected.forEach(({
|
|
184
|
+
address: rejectedAddress,
|
|
185
|
+
reason
|
|
186
|
+
}) => {
|
|
187
|
+
const message = reason || `${ERROR_PREFIX} Unknown exception processing routing result.`;
|
|
188
|
+
this.onAddressDone(rejectedAddress, 'rejected', [message]);
|
|
189
|
+
}); // if prefetch exclusively consists of unprocessable addresses, update state to done
|
|
190
|
+
|
|
191
|
+
if (rejected.length === adgRoutingResults.length) {
|
|
192
|
+
this.updateState(STATE.done, 'no processable addresses, do not bulkResolve and set state to "done"');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.bulkResolveGuard();
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Processes a list of addresses through the router in series
|
|
200
|
+
* @param addresses to process
|
|
201
|
+
* @returns { AdgRoutingResult[] } a list of AdgRoutingResult
|
|
202
|
+
*/
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
async processAllAddresses(addresses) {
|
|
206
|
+
// add instrumentation to get total duration to process all addresses
|
|
207
|
+
const action = InstrumentedAction.startAction('komaci.router', totalRoutingSchema, {
|
|
208
|
+
instrumentWithoutLog: false,
|
|
209
|
+
postTimeInLog: true,
|
|
210
|
+
apiOptions: this.apiOptions
|
|
211
|
+
});
|
|
212
|
+
const results = [];
|
|
213
|
+
const maxConcurrent = this.config.maxConcurrent || Number.MAX_SAFE_INTEGER;
|
|
214
|
+
|
|
215
|
+
for (let i = 0; i < addresses.length; i += maxConcurrent) {
|
|
216
|
+
const processList = addresses.slice(i, Math.min(addresses.length, i + maxConcurrent));
|
|
217
|
+
results.push(...(await Promise.all(processList.map(address => this.processAddress(address)))));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
action.finishAction(false, {});
|
|
221
|
+
return results;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Split all the addresses based on the maxConcurrent configuration
|
|
225
|
+
* bulkResolve them in iterations
|
|
226
|
+
*/
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
bulkResolveGuard() {
|
|
230
|
+
const maxConcurrent = this.config.maxConcurrent || Number.MAX_SAFE_INTEGER; // rest capacity when there are addresses being processed
|
|
231
|
+
|
|
232
|
+
let capacity = maxConcurrent;
|
|
233
|
+
const adgs = this.adgMap.keys();
|
|
234
|
+
|
|
235
|
+
for (const adgFn of adgs) {
|
|
236
|
+
const addresses = this.adgMap.get(adgFn);
|
|
237
|
+
|
|
238
|
+
if (addresses && addresses.length > 0) {
|
|
239
|
+
if (addresses.length > capacity) {
|
|
240
|
+
if (addresses.length < maxConcurrent) {
|
|
241
|
+
/* to benefits from batching,
|
|
242
|
+
we dont want to split addresses for same adg if they can be in same iteration
|
|
243
|
+
example: having maxConcurrent: 10
|
|
244
|
+
adg1: 6 addresses
|
|
245
|
+
adg2: 8 addresses
|
|
246
|
+
ideal way is two iterations for both adg1 and adg2 instead of iter1: 6 adg1 + 4 adg2, iter2: 4 adg2
|
|
247
|
+
in addition to the example, if there is another adg3: 4 addresses, will do
|
|
248
|
+
iter1: 6 adg1 + 4 adg3
|
|
249
|
+
iter2: 8 adg2
|
|
250
|
+
*/
|
|
251
|
+
continue;
|
|
252
|
+
} // if the length of address have exceed the maxConcurrent,
|
|
253
|
+
// split/resolve and update the rest to adgMap
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
const currentIteration = addresses.slice(0, capacity);
|
|
257
|
+
this.adgMap.set(adgFn, addresses.slice(capacity));
|
|
258
|
+
this.bulkResolve(adgFn, currentIteration);
|
|
259
|
+
break;
|
|
260
|
+
} else {
|
|
261
|
+
// if the length of address can fit in the capacity,
|
|
262
|
+
// resolve them all and remove the entry from adgMap
|
|
263
|
+
capacity = capacity - addresses.length;
|
|
264
|
+
this.adgMap.delete(adgFn);
|
|
265
|
+
this.bulkResolve(adgFn, addresses);
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
// in case of empty address list
|
|
269
|
+
this.adgMap.delete(adgFn);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Resolves the given adgFunction with the given resolutionGroup
|
|
275
|
+
* @param adgFn to resolve for the inputs provided in the resolutionContext
|
|
276
|
+
* @param addresses the list of addresses to be placed into a new BulkResolver
|
|
277
|
+
*/
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
bulkResolve(adgFn, addresses) {
|
|
281
|
+
if (this.state == STATE.stopped) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const action = InstrumentedAction.startAction('komaci', bulkResolveSchema, {
|
|
286
|
+
instrumentWithoutLog: false,
|
|
287
|
+
postTimeInLog: true,
|
|
288
|
+
apiOptions: this.apiOptions
|
|
289
|
+
});
|
|
290
|
+
const adgInputs = addresses.map(address => {
|
|
291
|
+
return {
|
|
292
|
+
properties: {},
|
|
293
|
+
context: {
|
|
294
|
+
CurrentPageReference: address
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
const resolverInputStatus = addresses.map(() => STATE.running);
|
|
299
|
+
const bulkResolutionGroup = {
|
|
300
|
+
addresses,
|
|
301
|
+
resolverInputStatus
|
|
302
|
+
};
|
|
303
|
+
bulkResolutionGroup.adgInputs = adgInputs;
|
|
304
|
+
bulkResolutionGroup.bulkResolver = getBulkAdgResolver(adgFn, adgInputs, this.onBulkResolutionStatus.bind(this, bulkResolutionGroup, action));
|
|
305
|
+
this.resolutionGroups.push(bulkResolutionGroup);
|
|
306
|
+
bulkResolutionGroup.bulkResolver.start();
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* StatusCallback to BulkResolver, invoked when resolver emits a new status.
|
|
310
|
+
* Updates the prefetchService's status if applicable.
|
|
311
|
+
* @param bulkResolutionGroup
|
|
312
|
+
* @param action InstrumentationContext
|
|
313
|
+
* @param status
|
|
314
|
+
*/
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
onBulkResolutionStatus(bulkResolutionGroup, action, status) {
|
|
318
|
+
const state = status.status.getState();
|
|
319
|
+
const index = status.index;
|
|
320
|
+
const {
|
|
321
|
+
addresses
|
|
322
|
+
} = bulkResolutionGroup;
|
|
323
|
+
|
|
324
|
+
if (index >= 0 && index < addresses.length) {
|
|
325
|
+
bulkResolutionGroup.resolverInputStatus[index] = state;
|
|
326
|
+
} else {
|
|
327
|
+
this.onUnexpectedCondition(`Invalid index provided by resolver: ${index}`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (bulkResolutionGroup.bulkResolver?.areAllResolversDone()) {
|
|
332
|
+
action.finishAction(false, {
|
|
333
|
+
userSchemaData: {
|
|
334
|
+
keyCount: addresses.length
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
switch (state) {
|
|
340
|
+
case STATE.done:
|
|
341
|
+
this.onResolutionDone(status, bulkResolutionGroup);
|
|
342
|
+
break;
|
|
343
|
+
|
|
344
|
+
case STATE.stopped:
|
|
345
|
+
// NOTE: Prefetch initiates stop, so this is a noop
|
|
346
|
+
break;
|
|
347
|
+
|
|
348
|
+
case STATE.running:
|
|
349
|
+
break;
|
|
350
|
+
|
|
351
|
+
default:
|
|
352
|
+
this.onUnexpectedCondition(`Unexpected resolution status: ${JSON.stringify(status)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Stops all bulkResolvers
|
|
357
|
+
*/
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
stop() {
|
|
361
|
+
if (this.state == STATE.stopped) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
this.resolutionGroups.forEach(bulkResolutionContext => {
|
|
366
|
+
const {
|
|
367
|
+
bulkResolver
|
|
368
|
+
} = bulkResolutionContext;
|
|
369
|
+
|
|
370
|
+
if (bulkResolver) {
|
|
371
|
+
bulkResolver.stop();
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
this.updateState(STATE.stopped);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Called upon unexpected conditions that may indicate to the consumer that
|
|
378
|
+
* the prefetchService or its associated resolver is in an invalid state
|
|
379
|
+
* @param message
|
|
380
|
+
*/
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
onUnexpectedCondition(message) {
|
|
384
|
+
// eslint-disable-next-line no-console
|
|
385
|
+
console.log(`Komaci/prefetch - error, message: ${message}`);
|
|
386
|
+
|
|
387
|
+
this._activity.error(message);
|
|
388
|
+
|
|
389
|
+
this.updateState(STATE.error, message);
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Called when resolution for a given address has completed, or was otherwise
|
|
393
|
+
* rejected.
|
|
394
|
+
* @param address that is done
|
|
395
|
+
* @param state completed when resolved, or rejected when either no ADG exists, or resolver emitted an error with the given address
|
|
396
|
+
* @param reasons reasons for rejected status, if any
|
|
397
|
+
*/
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
onAddressDone(address, state, reasons) {
|
|
401
|
+
const {
|
|
402
|
+
onAddressProcessed
|
|
403
|
+
} = this.config;
|
|
404
|
+
const status = {
|
|
405
|
+
state: state,
|
|
406
|
+
address,
|
|
407
|
+
reasons: reasons
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
onAddressProcessed?.(status);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
this.onUnexpectedCondition(error?.message);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Updates the current PrefetchState to done if every bulkResolver has emitted
|
|
418
|
+
* a non-running status for all inputs.
|
|
419
|
+
*/
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
updateStateIfAllDone(bulkResolutionContext, status) {
|
|
423
|
+
const bulkContexts = this.resolutionGroups.values();
|
|
424
|
+
let allDone = true;
|
|
425
|
+
|
|
426
|
+
for (const context of bulkContexts) {
|
|
427
|
+
if (context.bulkResolver && !context.bulkResolver.areAllResolversDone()) {
|
|
428
|
+
allDone = false;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (allDone) {
|
|
434
|
+
if (this.adgMap.size > 0) {
|
|
435
|
+
// if all current runs are all done but with addresses left in the adgMap
|
|
436
|
+
// go process them
|
|
437
|
+
this.bulkResolveGuard();
|
|
438
|
+
} else {
|
|
439
|
+
// prefetch is done when allIdel and nothing left in adgMap
|
|
440
|
+
// eslint-disable-next-line no-console
|
|
441
|
+
console.log(`Komaci/prefetch - duration: ${Date.now() - this._startTime}, size: ${this._size}.`);
|
|
442
|
+
|
|
443
|
+
this._activity.stop(prefetchServiceSchema, {
|
|
444
|
+
addressCount: this._size,
|
|
445
|
+
startTime: this._startTime
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
this.updateState(STATE.done, status.status.getErrorMessages().join('\n'));
|
|
449
|
+
bulkResolutionContext.bulkResolver = undefined;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Called when resolver emits a "sink" state for an address or all addresses
|
|
455
|
+
* @param status
|
|
456
|
+
* @param bulkResolutionContext
|
|
457
|
+
*/
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
onResolutionDone(status, bulkResolutionContext) {
|
|
461
|
+
const index = status.index;
|
|
462
|
+
const {
|
|
463
|
+
addresses
|
|
464
|
+
} = bulkResolutionContext;
|
|
465
|
+
const addressState = status.status.getErrorMessages().length !== 0 ? 'rejected' : 'completed';
|
|
466
|
+
const impactedAddress = addresses[index];
|
|
467
|
+
this.onAddressDone(impactedAddress, addressState, status.status.getErrorMessages());
|
|
468
|
+
this.updateStateIfAllDone(bulkResolutionContext, status);
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Updates PrefetchState and notifies consumer via config.onStateChanged callback
|
|
472
|
+
* @param newState the new state
|
|
473
|
+
* @param [message] state message, if any
|
|
474
|
+
*/
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
updateState(newState, message) {
|
|
478
|
+
const {
|
|
479
|
+
onStateChanged
|
|
480
|
+
} = this.config;
|
|
481
|
+
const abandoned = []; // TODO: Abandoned Not yet implemented
|
|
482
|
+
|
|
483
|
+
if (ValidStatusTransitions.isValid(this.state, newState)) {
|
|
484
|
+
this.state = newState;
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
onStateChanged?.({
|
|
488
|
+
state: newState,
|
|
489
|
+
abandoned,
|
|
490
|
+
message
|
|
491
|
+
});
|
|
492
|
+
} catch (error) {
|
|
493
|
+
// eslint-disable-next-line no-console
|
|
494
|
+
console.log(error?.message);
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
const errorMessage = `Komaci/prefetch - Invalid state transition from resovler. Resolver emitted state: ${newState} while prefetch is in ${this.state}`; // eslint-disable-next-line no-console
|
|
498
|
+
|
|
499
|
+
console.log(errorMessage);
|
|
500
|
+
|
|
501
|
+
this._activity.error(errorMessage, 'invalid-prefetch-state-transition');
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
getState() {
|
|
506
|
+
return this.state;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { PageReference } from 'lwr/router';
|
|
2
|
+
import type { ResolverExecutionState, AdgInput, AdgModule, IBulkResolver } from '@komaci/resolver';
|
|
3
|
+
import { InstrumentationContext } from 'o11y/dist/modules/o11y/client/interfaces';
|
|
4
|
+
export declare const ERROR_PREFIX = "[komaci prefetch]";
|
|
5
|
+
/**
|
|
6
|
+
* Routing result for each address
|
|
7
|
+
*/
|
|
8
|
+
export declare type AdgRoutingResult<TAddress = PageReference> = {
|
|
9
|
+
address: TAddress;
|
|
10
|
+
success: boolean;
|
|
11
|
+
adgModule?: AdgModule;
|
|
12
|
+
reason?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Addresses mapped to the same ADG are grouped together. Each grouping is
|
|
16
|
+
* associated with a bulkResolver which may emit state for either all inputs, or
|
|
17
|
+
* a single input. This interface is used by the prefetch service to track the
|
|
18
|
+
* state of the input addresses, associated adgInputs, the bulkResolver and
|
|
19
|
+
* statuses for each input.
|
|
20
|
+
*/
|
|
21
|
+
export declare type AdgResolutionGroup<TAddress = PageReference> = {
|
|
22
|
+
addresses: TAddress[];
|
|
23
|
+
adgInputs?: AdgInput[];
|
|
24
|
+
bulkResolver?: IBulkResolver;
|
|
25
|
+
resolverInputStatus: ResolverExecutionState[];
|
|
26
|
+
};
|
|
27
|
+
export interface IPrefetchService {
|
|
28
|
+
stop(): void;
|
|
29
|
+
}
|
|
30
|
+
export declare enum ResolutionStatusState {
|
|
31
|
+
running = "running",
|
|
32
|
+
done = "done",
|
|
33
|
+
error = "error",
|
|
34
|
+
stopped = "stopped"
|
|
35
|
+
}
|
|
36
|
+
export declare enum PrefetchStatusStates {
|
|
37
|
+
init = "init",
|
|
38
|
+
done = "done",
|
|
39
|
+
running = "running",
|
|
40
|
+
stopped = "stopped",
|
|
41
|
+
error = "error"
|
|
42
|
+
}
|
|
43
|
+
export declare enum AddressStatusStates {
|
|
44
|
+
completed = "completed",
|
|
45
|
+
rejected = "rejected"
|
|
46
|
+
}
|
|
47
|
+
export declare type PrefetchState = `${PrefetchStatusStates}`;
|
|
48
|
+
export declare type AddressState = `${AddressStatusStates}`;
|
|
49
|
+
export declare type AddressStatus<TAddress = PageReference> = {
|
|
50
|
+
state: AddressState;
|
|
51
|
+
address: TAddress;
|
|
52
|
+
reasons?: string[];
|
|
53
|
+
};
|
|
54
|
+
export declare type PrefetchStatus<TAddress = PageReference> = {
|
|
55
|
+
state: PrefetchState;
|
|
56
|
+
abandoned?: TAddress[];
|
|
57
|
+
message?: string;
|
|
58
|
+
};
|
|
59
|
+
export declare type PrefetchConfig<TAddress = PageReference> = {
|
|
60
|
+
/**
|
|
61
|
+
* Callback method to receive status notifications related to prefetch, i.e. 'running', 'done', see PrefetchStatusStates
|
|
62
|
+
*/
|
|
63
|
+
onStateChanged?: (status: PrefetchStatus<TAddress>) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Callback method to receive status notifications related to address resolution, see AddressStatusStates
|
|
66
|
+
*/
|
|
67
|
+
onAddressProcessed?: (status: AddressStatus<TAddress>) => void;
|
|
68
|
+
/**
|
|
69
|
+
* Callback method to download an image, given its URL, and return a Promise when finished
|
|
70
|
+
*/
|
|
71
|
+
downloadImage?: (imageURL: string) => Promise<unknown>;
|
|
72
|
+
/**
|
|
73
|
+
* Config parameter to control number of concurrent page resolutions and AdgInputs to BulkResolver.
|
|
74
|
+
*/
|
|
75
|
+
maxConcurrent?: number;
|
|
76
|
+
/**
|
|
77
|
+
* // o11y configuration, to ensure we can establish contexts for concurrent uses of o11y.
|
|
78
|
+
*/
|
|
79
|
+
instrumentationContext?: InstrumentationContext;
|
|
80
|
+
/**
|
|
81
|
+
* High volume priming is a special feature which causes us to only resolve each unique AdgModule once, even if multiple addresses for the module are passed.
|
|
82
|
+
*/
|
|
83
|
+
highVolumePriming?: boolean;
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=prefetchTypes.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const ERROR_PREFIX = '[komaci prefetch]';
|
|
2
|
+
/**
|
|
3
|
+
* Routing result for each address
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export let ResolutionStatusState;
|
|
7
|
+
|
|
8
|
+
(function (ResolutionStatusState) {
|
|
9
|
+
ResolutionStatusState["running"] = "running";
|
|
10
|
+
ResolutionStatusState["done"] = "done";
|
|
11
|
+
ResolutionStatusState["error"] = "error";
|
|
12
|
+
ResolutionStatusState["stopped"] = "stopped";
|
|
13
|
+
})(ResolutionStatusState || (ResolutionStatusState = {}));
|
|
14
|
+
|
|
15
|
+
export let PrefetchStatusStates;
|
|
16
|
+
|
|
17
|
+
(function (PrefetchStatusStates) {
|
|
18
|
+
PrefetchStatusStates["init"] = "init";
|
|
19
|
+
PrefetchStatusStates["done"] = "done";
|
|
20
|
+
PrefetchStatusStates["running"] = "running";
|
|
21
|
+
PrefetchStatusStates["stopped"] = "stopped";
|
|
22
|
+
PrefetchStatusStates["error"] = "error";
|
|
23
|
+
})(PrefetchStatusStates || (PrefetchStatusStates = {}));
|
|
24
|
+
|
|
25
|
+
export let AddressStatusStates;
|
|
26
|
+
|
|
27
|
+
(function (AddressStatusStates) {
|
|
28
|
+
AddressStatusStates["completed"] = "completed";
|
|
29
|
+
AddressStatusStates["rejected"] = "rejected";
|
|
30
|
+
})(AddressStatusStates || (AddressStatusStates = {}));
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@komaci/prefetch",
|
|
3
|
+
"version": "240.1.3",
|
|
4
|
+
"description": "Komaci prefetch service.",
|
|
5
|
+
"homepage": "https://komaci.dev/",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/salesforce/komaci.git",
|
|
9
|
+
"directory": "packages/@komaci/prefetch"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/salesforce/komaci/issues"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"types": "build/modules/komaci/prefetchTypes/prefetchTypes.d.ts",
|
|
20
|
+
"main": "build/index.js",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "node ../../../bin/pack-ts --dir src/modules build/modules && tsc -b ./tsconfig.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"build/**/*.js",
|
|
26
|
+
"build/**/*.d.ts"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@komaci/module-shared": "240.1.3",
|
|
30
|
+
"@komaci/resolver": "240.1.3",
|
|
31
|
+
"@lwrjs/router": "0.6.0-alpha.15",
|
|
32
|
+
"o11y": "^240.7.0",
|
|
33
|
+
"o11y_schema": "^240.11.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@komaci/types": "240.1.3",
|
|
37
|
+
"wait-for-expect": "^3.0.2"
|
|
38
|
+
}
|
|
39
|
+
}
|