-
Notifications
You must be signed in to change notification settings - Fork 3
feat(davinci-client): add polling support (SDKS-4687) #563
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,58 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * This software may be modified and distributed under the terms | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * of the MIT license. See the LICENSE file for details. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PollingCollector, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PollingStatus, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| InternalErrorResponse, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Updater, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '@forgerock/davinci-client/types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function pollingComponent( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| formEl: HTMLFormElement, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| collector: PollingCollector, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| poll: (collector: PollingCollector) => Promise<PollingStatus | InternalErrorResponse>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updater: Updater<PollingCollector>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| submitForm: () => Promise<void>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const button = document.createElement('button'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| button.type = 'button'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| button.value = collector.output.key; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| button.innerHTML = 'Start polling'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| formEl.appendChild(button); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| button.onclick = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const p = document.createElement('p'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p.innerText = 'Polling...'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| formEl?.appendChild(p); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const status = await poll(collector); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof status !== 'string' && 'error' in status) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(status.error?.message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const errEl = document.createElement('p'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| errEl.innerText = 'Polling error: ' + status.error?.message; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| formEl?.appendChild(errEl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = updater(status); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (result && 'error' in result) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(result.error.message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const errEl = document.createElement('p'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| errEl.innerText = 'Polling error: ' + result.error.message; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| formEl?.appendChild(errEl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const resultEl = document.createElement('p'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resultEl.innerText = 'Polling result: ' + JSON.stringify(status, null, 2); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| formEl?.appendChild(resultEl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await submitForm(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Block re-entry while a poll is in flight. The button stays clickable during both 💡 Proposed fix button.onclick = async () => {
- const p = document.createElement('p');
- p.innerText = 'Polling...';
- formEl?.appendChild(p);
-
- const status = await poll(collector);
- if (typeof status !== 'string' && 'error' in status) {
- console.error(status.error?.message);
-
- const errEl = document.createElement('p');
- errEl.innerText = 'Polling error: ' + status.error?.message;
- formEl?.appendChild(errEl);
- return;
- }
-
- const result = updater(status);
- if (result && 'error' in result) {
- console.error(result.error.message);
-
- const errEl = document.createElement('p');
- errEl.innerText = 'Polling error: ' + result.error.message;
- formEl?.appendChild(errEl);
- return;
- }
-
- const resultEl = document.createElement('p');
- resultEl.innerText = 'Polling result: ' + JSON.stringify(status, null, 2);
- formEl?.appendChild(resultEl);
-
- await submitForm();
+ button.disabled = true;
+ try {
+ const p = document.createElement('p');
+ p.innerText = 'Polling...';
+ formEl?.appendChild(p);
+
+ const status = await poll(collector);
+ if (typeof status !== 'string' && 'error' in status) {
+ console.error(status.error?.message);
+
+ const errEl = document.createElement('p');
+ errEl.innerText = 'Polling error: ' + status.error?.message;
+ formEl?.appendChild(errEl);
+ return;
+ }
+
+ const result = updater(status);
+ if (result && 'error' in result) {
+ console.error(result.error.message);
+
+ const errEl = document.createElement('p');
+ errEl.innerText = 'Polling error: ' + result.error.message;
+ formEl?.appendChild(errEl);
+ return;
+ }
+
+ const resultEl = document.createElement('p');
+ resultEl.innerText = 'Polling result: ' + JSON.stringify(status, null, 2);
+ formEl?.appendChild(resultEl);
+
+ await submitForm();
+ } finally {
+ button.disabled = false;
+ }
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,14 +4,20 @@ | |
| * This software may be modified and distributed under the terms | ||
| * of the MIT license. See the LICENSE file for details. | ||
| */ | ||
| /** | ||
| * Import RTK slices and api | ||
| */ | ||
| import { Micro } from 'effect'; | ||
| import { CustomLogger, logger as loggerFn, LogLevel } from '@forgerock/sdk-logger'; | ||
| import { createStorage } from '@forgerock/storage'; | ||
| import { isGenericError, createWellknownError } from '@forgerock/sdk-utilities'; | ||
|
|
||
| import { createClientStore, handleUpdateValidateError, RootState } from './client.store.utils.js'; | ||
| /** | ||
| * Import RTK slices and api | ||
| */ | ||
| import { | ||
| createClientStore, | ||
| handleChallengePolling, | ||
| handleUpdateValidateError, | ||
| RootState, | ||
| } from './client.store.utils.js'; | ||
| import { nodeSlice } from './node.slice.js'; | ||
| import { davinciApi } from './davinci.api.js'; | ||
| import { configSlice } from './config.slice.js'; | ||
|
|
@@ -34,6 +40,7 @@ import type { | |
| ObjectValueCollectors, | ||
| PhoneNumberInputValue, | ||
| AutoCollectors, | ||
| PollingCollector, | ||
| MultiValueCollectors, | ||
| FidoRegistrationInputValue, | ||
| FidoAuthenticationInputValue, | ||
|
|
@@ -44,6 +51,7 @@ import type { | |
| NodeStates, | ||
| Updater, | ||
| Validator, | ||
| PollingStatus, | ||
| } from './client.types.js'; | ||
| import { returnValidator } from './collector.utils.js'; | ||
| import type { ContinueNode, StartNode } from './node.types.js'; | ||
|
|
@@ -404,6 +412,85 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({ | |
| return returnValidator(collectorToUpdate); | ||
| }, | ||
|
|
||
| /** | ||
| * @method poll - Perform challenge polling or continue polling | ||
| * @param {PollingCollector} collector - the polling collector | ||
| * @returns {Promise<PollingStatus | InternalErrorResponse>} - Returns a promise that resolves to a polling status or error | ||
| */ | ||
| poll: async (collector: PollingCollector): Promise<PollingStatus | InternalErrorResponse> => { | ||
| try { | ||
| if (collector.type !== 'PollingCollector') { | ||
| log.error('Collector provided to poll is not a PollingCollector'); | ||
| return { | ||
| error: { | ||
| message: 'Collector provided to poll is not a PollingCollector', | ||
| type: 'argument_error', | ||
| }, | ||
| type: 'internal_error', | ||
| }; | ||
| } | ||
|
|
||
| const pollChallengeStatus = collector.output.config.pollChallengeStatus; | ||
| const challenge = collector.output.config.challenge; | ||
|
|
||
| if (challenge && pollChallengeStatus === true) { | ||
| // Challenge Polling | ||
| return await handleChallengePolling({ | ||
| collector, | ||
| challenge, | ||
| store, | ||
| log, | ||
| }); | ||
| } else if (!challenge && !pollChallengeStatus) { | ||
| // Continue polling | ||
| const retriesLeft = collector.output.config.retriesRemaining; | ||
| const pollInterval = collector.output.config.pollInterval ?? 2000; // miliseconds | ||
|
|
||
| if (retriesLeft === undefined) { | ||
| log.error('No retries found on PollingCollector'); | ||
| return { | ||
| error: { | ||
| message: 'No retries found on PollingCollector', | ||
| type: 'argument_error', | ||
| }, | ||
| type: 'internal_error', | ||
| }; | ||
| } | ||
|
|
||
| if (retriesLeft > 0) { | ||
| const getStatusµ = Micro.sync(() => 'continue' as PollingStatus).pipe( | ||
| Micro.delay(pollInterval), | ||
| ); | ||
| const status: PollingStatus = await Micro.runPromise(getStatusµ); | ||
| return status; | ||
| } else { | ||
| // Retries exhausted | ||
| return 'timedOut' as PollingStatus; | ||
| } | ||
| } else { | ||
| // Error if polling type can't be determined from configuration | ||
| log.error('Invalid polling collector configuration'); | ||
| return { | ||
| error: { | ||
| message: 'Invalid polling collector configuration', | ||
| type: 'internal_error', | ||
| }, | ||
| type: 'internal_error', | ||
| }; | ||
| } | ||
|
Comment on lines
+420
to
+480
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolve the polling collector from store before using its config. Unlike 💡 Proposed fix poll: async (collector: PollingCollector): Promise<PollingStatus | InternalErrorResponse> => {
try {
if (collector.type !== 'PollingCollector') {
log.error('Collector provided to poll is not a PollingCollector');
return {
@@
type: 'internal_error',
};
}
- const pollChallengeStatus = collector.output.config.pollChallengeStatus;
- const challenge = collector.output.config.challenge;
+ const { error, state: currentCollector } = nodeSlice.selectors.selectCollector(
+ store.getState(),
+ collector.id,
+ );
+
+ if (error || !currentCollector || currentCollector.type !== 'PollingCollector') {
+ log.error('PollingCollector not found in current state');
+ return {
+ error: {
+ message: 'PollingCollector not found in current state',
+ type: 'state_error',
+ },
+ type: 'internal_error',
+ };
+ }
+
+ const pollChallengeStatus = currentCollector.output.config.pollChallengeStatus;
+ const challenge = currentCollector.output.config.challenge;
if (challenge && pollChallengeStatus === true) {
// Challenge Polling
return await handleChallengePolling({
- collector,
+ collector: currentCollector,
challenge,
store,
log,
});
} else if (!challenge && !pollChallengeStatus) {
// Continue polling
- const retriesLeft = collector.output.config.retriesRemaining;
- const pollInterval = collector.output.config.pollInterval ?? 2000; // miliseconds
+ const retriesLeft = currentCollector.output.config.retriesRemaining;
+ const pollInterval = currentCollector.output.config.pollInterval ?? 2000; // miliseconds🤖 Prompt for AI Agents |
||
| } catch (err) { | ||
| const errorMessage = err instanceof Error ? err.message : String(err); | ||
| log.error(errorMessage); | ||
| return { | ||
| error: { | ||
| message: errorMessage || 'An unexpected error occurred during poll operation', | ||
| type: 'internal_error', | ||
| }, | ||
| type: 'internal_error', | ||
| }; | ||
| } | ||
| }, | ||
|
|
||
| /** | ||
| * @method client - Selector to get the node.client from state | ||
| * @returns {Node.client} - the client property from the current node | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: ForgeRock/ping-javascript-sdk
Length of output: 221
🏁 Script executed:
Repository: ForgeRock/ping-javascript-sdk
Length of output: 416
🏁 Script executed:
Repository: ForgeRock/ping-javascript-sdk
Length of output: 219
🏁 Script executed:
Repository: ForgeRock/ping-javascript-sdk
Length of output: 261
Pin npm versions in CI instead of using
npm@latest.Found two locations installing npm without a pinned version:
.github/actions/setup/action.yml:36—corepack install -g npm@latest.github/workflows/publish.yml:54—npm install npm@latest -gUsing
npm@latestmakes CI non-reproducible and can break unexpectedly when npm releases. Pin to a specific version for deterministic builds.🤖 Prompt for AI Agents