@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.
@@ -0,0 +1,5 @@
1
+ /// <reference types="jest" />
2
+ import type { PageReference, Router } from 'lwr/router';
3
+ export declare const createRouter: jest.Mock<any, any>;
4
+ export type { PageReference, Router };
5
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1,5 @@
1
+ /// <reference types="jest" />
2
+ import type { PageReference, Router } from 'lwr/router';
3
+ export declare const createRouter: jest.Mock<any, any>;
4
+ export type { PageReference, Router };
5
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1,3 @@
1
+ declare const _default: import("packages/@komaci/resolver/src/modules/komaci/resolverTypes/resolverTypes").AdgModuleHandle | undefined;
2
+ export default _default;
3
+ //# sourceMappingURL=mockAdgModule.d.ts.map
@@ -0,0 +1,3 @@
1
+ /// <reference types="jest" />
2
+ export declare const getInstrumentation: jest.Mock<any, any>;
3
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,5 @@
1
+ export declare const wireContextSchema: any;
2
+ export declare const batchSchema: any;
3
+ export declare const bulkResolveSchema: any;
4
+ export declare const prefetchSchema: any;
5
+ //# sourceMappingURL=sf_komaci.d.ts.map
@@ -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
+ }