Skip to content

Commit e5ad583

Browse files
committed
feat(davinci-client): add continue polling support
1 parent 3113110 commit e5ad583

14 files changed

Lines changed: 143 additions & 23 deletions

File tree

.github/actions/setup/action.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ runs:
3131
registry-url: 'https://registry.npmjs.org'
3232

3333
- name: Update npm
34-
run: npm install -g npm@latest
34+
run: |
35+
corepack enable
36+
corepack install -g npm@latest
3537
shell: bash
3638

3739
- name: Install dependencies

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
with:
3333
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
3434

35-
- run: npx nx-cloud fix-ci
35+
- run: pnpm nx fix-ci
3636
if: always()
3737

3838
- uses: codecov/codecov-action@v5

e2e/davinci-app/components/polling.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
66
*/
7-
import {
7+
import type {
88
PollingCollector,
99
PollingStatus,
1010
InternalErrorResponse,
@@ -31,13 +31,21 @@ export default function pollingComponent(
3131

3232
const status = await poll(collector);
3333
if (typeof status !== 'string' && 'error' in status) {
34-
console.error(status.error.message);
34+
console.error(status.error?.message);
35+
36+
const errEl = document.createElement('p');
37+
errEl.innerText = 'Polling error: ' + status.error?.message;
38+
formEl?.appendChild(errEl);
3539
return;
3640
}
3741

3842
const result = updater(status);
3943
if (result && 'error' in result) {
4044
console.error(result.error.message);
45+
46+
const errEl = document.createElement('p');
47+
errEl.innerText = 'Polling error: ' + result.error.message;
48+
formEl?.appendChild(errEl);
4149
return;
4250
}
4351

packages/davinci-client/src/lib/client.store.ts

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
66
*/
7-
/**
8-
* Import RTK slices and api
9-
*/
7+
import { Micro } from 'effect';
108
import { CustomLogger, logger as loggerFn, LogLevel } from '@forgerock/sdk-logger';
119
import { createStorage } from '@forgerock/storage';
1210
import { isGenericError, createWellknownError } from '@forgerock/sdk-utilities';
1311

12+
/**
13+
* Import RTK slices and api
14+
*/
1415
import {
1516
createClientStore,
1617
handleChallengePolling,
@@ -412,7 +413,9 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
412413
},
413414

414415
/**
415-
* @method: poll - Poll for updates for a polling collector
416+
* @method poll - Perform challenge polling or continue polling
417+
* @param {PollingCollector} collector - the polling collector
418+
* @returns {Promise<PollingStatus | InternalErrorResponse>} - Returns a promise that resolves to a polling status or error
416419
*/
417420
poll: async (collector: PollingCollector): Promise<PollingStatus | InternalErrorResponse> => {
418421
try {
@@ -427,19 +430,53 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
427430
};
428431
}
429432

433+
const pollChallengeStatus = collector.output.config.pollChallengeStatus;
430434
const challenge = collector.output.config.challenge;
431435

432-
// Challenge Polling
433-
if (challenge) {
436+
if (challenge && pollChallengeStatus === true) {
437+
// Challenge Polling
434438
return await handleChallengePolling({
435439
collector,
436440
challenge,
437441
store,
438442
log,
439443
});
444+
} else if (!challenge && !pollChallengeStatus) {
445+
// Continue polling
446+
const retriesLeft = collector.output.config.retriesRemaining;
447+
const pollInterval = collector.output.config.pollInterval ?? 2000; // miliseconds
448+
449+
if (retriesLeft === undefined) {
450+
log.error('No retries found on PollingCollector');
451+
return {
452+
error: {
453+
message: 'No retries found on PollingCollector',
454+
type: 'argument_error',
455+
},
456+
type: 'internal_error',
457+
};
458+
}
459+
460+
if (retriesLeft > 0) {
461+
const getStatusµ = Micro.sync(() => 'continue' as PollingStatus).pipe(
462+
Micro.delay(pollInterval),
463+
);
464+
const status: PollingStatus = await Micro.runPromise(getStatusµ);
465+
return status;
466+
} else {
467+
// Retries exhausted
468+
return 'timedOut' as PollingStatus;
469+
}
440470
} else {
441-
// TODO: Handle continue polling
442-
return 'error' as PollingStatus;
471+
// Error if polling type can't be determined from configuration
472+
log.error('Invalid polling collector configuration');
473+
return {
474+
error: {
475+
message: 'Invalid polling collector configuration',
476+
type: 'internal_error',
477+
},
478+
type: 'internal_error',
479+
};
443480
}
444481
} catch (err) {
445482
const errorMessage = err instanceof Error ? err.message : String(err);

packages/davinci-client/src/lib/client.store.utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export async function handleChallengePolling({
183183
retriesLeft--;
184184
return store.dispatch(
185185
davinciApi.endpoints.poll.initiate({
186-
challengeEndpoint,
186+
endpoint: challengeEndpoint,
187187
interactionId,
188188
}),
189189
);

packages/davinci-client/src/lib/client.types.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,11 @@ export type Validator = (value: string) =>
9292

9393
export type NodeStates = StartNode | ContinueNode | ErrorNode | SuccessNode | FailureNode;
9494

95-
export type PollingStatusComplete = 'approved' | 'denied' | 'continue' | string;
96-
export type PollingStatus = PollingStatusComplete | 'expired' | 'timedOut' | 'error';
95+
export type PollingStatusChallengeComplete = 'approved' | 'denied' | 'continue' | string;
96+
export type PollingStatusChallenge =
97+
| PollingStatusChallengeComplete
98+
| 'expired'
99+
| 'timedOut'
100+
| 'error';
101+
export type PollingStatusContinue = 'continue' | 'timedOut';
102+
export type PollingStatus = PollingStatusContinue | PollingStatusChallenge;

packages/davinci-client/src/lib/collector.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,7 @@ export interface PollingOutputValue {
588588
pollRetries: number;
589589
pollChallengeStatus?: boolean;
590590
challenge?: string;
591+
retriesRemaining?: number;
591592
}
592593

593594
export type AutoCollectorCategories = 'SingleValueAutoCollector' | 'ObjectValueAutoCollector';

packages/davinci-client/src/lib/collector.utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ export function returnSingleValueAutoCollector<
326326
pollChallengeStatus: field.pollChallengeStatus,
327327
}),
328328
...(field.challenge && { challenge: field.challenge }),
329+
...(!field.challenge && { retriesRemaining: field.pollRetries }),
329330
},
330331
},
331332
} as InferAutoCollectorType<'PollingCollector'>;

packages/davinci-client/src/lib/davinci.api.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -430,19 +430,19 @@ export const davinciApi = createApi({
430430
}),
431431

432432
/**
433-
* @method poll - method for polling in a DaVinci flow
433+
* @method poll - method for challenge polling in a DaVinci flow
434434
*/
435435
/**
436436
* The poll endpoint differs from others in that it does not use onQueryStarted. This allows
437-
* us to use the response from onQueryFn, while avoiding updating the node state with the poll
437+
* us to use the response from queryFn, while avoiding updating the node state with the poll
438438
* response which causes the node to lose the initial collectors state when polling started.
439439
*/
440-
poll: builder.mutation<unknown, { challengeEndpoint: string; interactionId: string }>({
441-
async queryFn({ challengeEndpoint, interactionId }, api, _c, baseQuery) {
440+
poll: builder.mutation<unknown, { endpoint: string; interactionId: string }>({
441+
async queryFn({ endpoint, interactionId }, api, _c, baseQuery) {
442442
const { requestMiddleware, logger } = api.extra as Extras;
443443

444444
const request: FetchArgs = {
445-
url: challengeEndpoint,
445+
url: endpoint,
446446
credentials: 'include',
447447
method: 'POST',
448448
headers: {

packages/davinci-client/src/lib/davinci.types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,17 @@ export interface DaVinciNextResponse extends DaVinciBaseResponse {
264264
};
265265
}
266266

267+
/**
268+
* Continue Polling Response DaVinci API
269+
*/
270+
271+
export interface DaVinciPollResponse extends DaVinciBaseResponse {
272+
// Optional properties
273+
eventName?: string;
274+
success?: boolean;
275+
_links?: Links;
276+
}
277+
267278
/**
268279
* Error Response from DaVinci API
269280
*/

0 commit comments

Comments
 (0)