Skip to content

Commit

Permalink
Added support for map based DialogState
Browse files Browse the repository at this point in the history
  • Loading branch information
Stevenic committed Jan 10, 2019
1 parent 586c7cc commit ec780e5
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 202 deletions.
4 changes: 2 additions & 2 deletions libraries/botbuilder-dialogs/src/componentDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class ComponentDialog<O extends object = {}> extends Dialog<O> {
// Start the inner dialog.
const dialogState: DialogState = { dialogStack: [], componentValues: {} };
outerDC.activeDialog.state[PERSISTED_DIALOG_STATE] = dialogState;
const innerDC: DialogContext = new DialogContext(this.dialogs, outerDC.context, dialogState, outerDC.sessionValues);
const innerDC: DialogContext = new DialogContext(this.dialogs, outerDC.context, dialogState, outerDC.sessionState);
const turnResult: DialogTurnResult<any> = await this.onBeginDialog(innerDC, options);

// Check for end of inner dialog
Expand All @@ -99,7 +99,7 @@ export class ComponentDialog<O extends object = {}> extends Dialog<O> {
public async continueDialog(outerDC: DialogContext): Promise<DialogTurnResult> {
// Continue execution of inner dialog.
const dialogState: any = outerDC.activeDialog.state[PERSISTED_DIALOG_STATE];
const innerDC: DialogContext = new DialogContext(this.dialogs, outerDC.context, dialogState, outerDC.sessionValues);
const innerDC: DialogContext = new DialogContext(this.dialogs, outerDC.context, dialogState, outerDC.sessionState);
const turnResult: DialogTurnResult<any> = await this.onContinueDialog(innerDC);

// Check for end of inner dialog
Expand Down
5 changes: 2 additions & 3 deletions libraries/botbuilder-dialogs/src/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import { DialogContext } from './dialogContext';

/**
* Tracking information persisted for an instance of a dialog on the stack.
* @param T (Optional) type of state being persisted for the dialog.
*/
export interface DialogInstance<T = any> {
export interface DialogInstance {
/**
* ID of the dialog this instance is for.
*/
Expand All @@ -21,7 +20,7 @@ export interface DialogInstance<T = any> {
/**
* The instances persisted state.
*/
state: T;
state: object;
}

/**
Expand Down
40 changes: 21 additions & 19 deletions libraries/botbuilder-dialogs/src/dialogContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Choice } from './choices';
import { Dialog, DialogInstance, DialogReason, DialogTurnResult, DialogTurnStatus } from './dialog';
import { DialogSet } from './dialogSet';
import { PromptOptions } from './prompts';
import { ValueMap } from './valueMap';
import { StateMap } from './stateMap';

/**
* State information persisted by a `DialogSet`.
Expand Down Expand Up @@ -64,33 +64,33 @@ export class DialogContext {
/**
* Persisted values that are visible across all of the bots components.
*/
public readonly sessionValues: ValueMap;
public readonly sessionState: StateMap;

/**
* Persisted values that are visible across all of the dialogs for the current component.
*/
public readonly componentValues: ValueMap;
public readonly componentState: StateMap;

/**
* Creates a new DialogContext instance.
* @param dialogs Parent dialog set.
* @param context Context for the current turn of conversation with the user.
* @param state State object being used to persist the dialog stack.
* @param sessionValues (Optional) session values to bind context to. Session values will be persisted off the `state` property if not specified.
* @param sessionState (Optional) session values to bind context to. Session values will be persisted off the `state` property if not specified.
*/
constructor(dialogs: DialogSet, context: TurnContext, state: DialogState, sessionValues?: ValueMap) {
constructor(dialogs: DialogSet, context: TurnContext, state: DialogState, sessionState?: StateMap) {
if (!Array.isArray(state.dialogStack)) { state.dialogStack = []; }
if (typeof state.componentValues !== 'object') { state.componentValues = {}; }
this.dialogs = dialogs;
this.context = context;
this.stack = state.dialogStack;
this.componentValues = new ValueMap(state.componentValues);
if (!sessionValues) {
// Create a new session values map
this.componentState = new StateMap(state.componentValues);
if (!sessionState) {
// Create a new session state map
if (typeof (state as RootDialogState).sessionValues !== 'object') { (state as RootDialogState).sessionValues = {}; }
sessionValues = new ValueMap((state as RootDialogState).sessionValues);
sessionState = new StateMap((state as RootDialogState).sessionValues);
}
this.sessionValues = sessionValues;
this.sessionState = sessionState;
}

/**
Expand All @@ -99,15 +99,17 @@ export class DialogContext {
*
* @remarks
* Dialogs can use this to persist custom state in between conversation turns:
*
* ```JavaScript
* dc.activeDialog.state = { someFlag: true };
* ```
*/
public get activeDialog(): DialogInstance|undefined {
return this.stack.length > 0 ? this.stack[this.stack.length - 1] : undefined;
}

public get dialogState(): StateMap {
const instance = this.activeDialog;
if (!instance) { throw new Error(`DialogContext.dialogState: no active dialog instance.`); }
return new StateMap(instance.state);
}

/**
* Pushes a new dialog onto the dialog stack.
*
Expand All @@ -131,7 +133,7 @@ export class DialogContext {
if (!dialog) { throw new Error(`DialogContext.beginDialog(): A dialog with an id of '${dialogId}' wasn't found.`); }

// Push new instance onto stack.
const instance: DialogInstance<any> = {
const instance: DialogInstance = {
id: dialogId,
state: {}
};
Expand Down Expand Up @@ -225,7 +227,7 @@ export class DialogContext {
*/
public async continueDialog(): Promise<DialogTurnResult> {
// Check for a dialog on the stack
const instance: DialogInstance<any> = this.activeDialog;
const instance: DialogInstance = this.activeDialog;
if (instance) {
// Lookup dialog
const dialog: Dialog<{}> = this.dialogs.find(instance.id);
Expand Down Expand Up @@ -265,7 +267,7 @@ export class DialogContext {
await this.endActiveDialog(DialogReason.endCalled);

// Resume parent dialog
const instance: DialogInstance<any> = this.activeDialog;
const instance: DialogInstance = this.activeDialog;
if (instance) {
// Lookup dialog
const dialog: Dialog<{}> = this.dialogs.find(instance.id);
Expand Down Expand Up @@ -332,7 +334,7 @@ export class DialogContext {
*/
public async repromptDialog(): Promise<void> {
// Check for a dialog on the stack
const instance: DialogInstance<any> = this.activeDialog;
const instance: DialogInstance = this.activeDialog;
if (instance) {
// Lookup dialog
const dialog: Dialog<{}> = this.dialogs.find(instance.id);
Expand All @@ -346,7 +348,7 @@ export class DialogContext {
}

private async endActiveDialog(reason: DialogReason): Promise<void> {
const instance: DialogInstance<any> = this.activeDialog;
const instance: DialogInstance = this.activeDialog;
if (instance) {
// Lookup dialog
const dialog: Dialog<{}> = this.dialogs.find(instance.id);
Expand Down
2 changes: 1 addition & 1 deletion libraries/botbuilder-dialogs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ export * from './dialog';
export * from './componentDialog';
export * from './dialogContext';
export * from './dialogSet';
export * from './valueMap';
export * from './stateMap';
export * from './waterfallDialog';
export * from './waterfallStepContext';
33 changes: 18 additions & 15 deletions libraries/botbuilder-dialogs/src/prompts/activityPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Activity, InputHints, TurnContext, ActivityTypes } from 'botbuilder-cor
import { Dialog, DialogInstance, DialogReason, DialogTurnResult } from '../dialog';
import { DialogContext } from '../dialogContext';
import { PromptOptions, PromptRecognizerResult, PromptValidator } from './prompt';
import { StateMap } from '../stateMap';

/**
* Waits for an activity to be received.
Expand Down Expand Up @@ -40,37 +41,37 @@ export abstract class ActivityPrompt extends Dialog {
}

// Initialize prompt state
const state: any = dc.activeDialog.state as ActivityPromptState;
state.options = opt;
state.state = {};
const state = dc.dialogState;
state.set(PERSISTED_OPTIONS, opt);
state.set(PERSISTED_STATE, {});

// Send initial prompt
await this.onPrompt(dc.context, state.state, state.options, false);
await this.onPrompt(dc.context, state.get(PERSISTED_STATE), state.get(PERSISTED_OPTIONS), false);

return Dialog.EndOfTurn;
}

public async continueDialog(dc: DialogContext): Promise<DialogTurnResult> {
// Perform base recognition
const state: any = dc.activeDialog.state as ActivityPromptState;
const recognized: PromptRecognizerResult<Activity> = await this.onRecognize(dc.context, state.state, state.options);
const state = dc.dialogState;
const recognized: PromptRecognizerResult<Activity> = await this.onRecognize(dc.context, state.get(PERSISTED_STATE), state.get(PERSISTED_OPTIONS));

// Validate the return value
// - Unlike the other prompts a validator is required for an ActivityPrompt so we don't
// need to check for its existence before calling it.
const isValid: boolean = await this.validator({
context: dc.context,
recognized: recognized,
state: state.state,
options: state.options
state: state.get(PERSISTED_STATE),
options: state.get(PERSISTED_OPTIONS)
});

// Return recognized value or re-prompt
if (isValid) {
return await dc.endDialog(recognized.value);
} else {
if (dc.context.activity.type === ActivityTypes.Message && !dc.context.responded) {
await this.onPrompt(dc.context, state.state, state.options, true);
await this.onPrompt(dc.context, state.get(PERSISTED_STATE), state.get(PERSISTED_OPTIONS), true);
}

return Dialog.EndOfTurn;
Expand All @@ -89,8 +90,8 @@ export abstract class ActivityPrompt extends Dialog {
}

public async repromptDialog(context: TurnContext, instance: DialogInstance): Promise<void> {
const state: ActivityPromptState = instance.state as ActivityPromptState;
await this.onPrompt(context, state.state, state.options, true);
const state = new StateMap(instance.state);
await this.onPrompt(context, state.get(PERSISTED_STATE), state.get(PERSISTED_OPTIONS), true);
}

protected async onPrompt(context: TurnContext, state: object, options: PromptOptions, isRetry: boolean): Promise<void> {
Expand All @@ -109,7 +110,9 @@ export abstract class ActivityPrompt extends Dialog {
/**
* @private
*/
interface ActivityPromptState {
state: object;
options: PromptOptions;
}
const PERSISTED_OPTIONS = 'options';

/**
* @private
*/
const PERSISTED_STATE = 'state';
39 changes: 23 additions & 16 deletions libraries/botbuilder-dialogs/src/prompts/oauthPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@ export class OAuthPrompt extends Dialog {

// Initialize prompt state
const timeout: number = typeof this.settings.timeout === 'number' ? this.settings.timeout : 54000000;
const state: OAuthPromptState = dc.activeDialog.state as OAuthPromptState;
state.state = {};
state.options = o;
state.expires = new Date().getTime() + timeout;
const state = dc.dialogState;
state.set(PERSISTED_STATE, {});
state.set(PERSISTED_OPTIONS, o);
state.set(PERSISTED_EXPIRES, new Date().getTime() + timeout);

// Attempt to get the users token
const output: TokenResponse = await this.getUserToken(dc.context);
Expand All @@ -139,7 +139,7 @@ export class OAuthPrompt extends Dialog {
return await dc.endDialog(output);
} else {
// Prompt user to login
await this.sendOAuthCardAsync(dc.context, state.options.prompt);
await this.sendOAuthCardAsync(dc.context, state.get(PERSISTED_OPTIONS).prompt);

return Dialog.EndOfTurn;
}
Expand All @@ -150,9 +150,9 @@ export class OAuthPrompt extends Dialog {
const recognized: PromptRecognizerResult<TokenResponse> = await this.recognizeToken(dc.context);

// Check for timeout
const state: OAuthPromptState = dc.activeDialog.state as OAuthPromptState;
const state = dc.dialogState;
const isMessage: boolean = dc.context.activity.type === ActivityTypes.Message;
const hasTimedOut: boolean = isMessage && (new Date().getTime() > state.expires);
const hasTimedOut: boolean = isMessage && (new Date().getTime() > state.get(PERSISTED_EXPIRES));
if (hasTimedOut) {
return await dc.endDialog(undefined);
} else {
Expand All @@ -162,8 +162,8 @@ export class OAuthPrompt extends Dialog {
isValid = await this.validator({
context: dc.context,
recognized: recognized,
state: state.state,
options: state.options
state: state.get(PERSISTED_STATE),
options: state.get(PERSISTED_OPTIONS)
});
} else if (recognized.succeeded) {
isValid = true;
Expand All @@ -174,8 +174,8 @@ export class OAuthPrompt extends Dialog {
return await dc.endDialog(recognized.value);
} else {
// Send retry prompt
if (!dc.context.responded && isMessage && state.options.retryPrompt) {
await dc.context.sendActivity(state.options.retryPrompt);
if (!dc.context.responded && isMessage && state.get(PERSISTED_OPTIONS).retryPrompt) {
await dc.context.sendActivity(state.get(PERSISTED_OPTIONS).retryPrompt);
}

return Dialog.EndOfTurn;
Expand Down Expand Up @@ -313,8 +313,15 @@ export class OAuthPrompt extends Dialog {
/**
* @private
*/
interface OAuthPromptState {
state: object;
options: PromptOptions;
expires: number; // Timestamp of when the prompt will timeout.
}
const PERSISTED_STATE = 'state';

/**
* @private
*/
const PERSISTED_OPTIONS = 'options';

/**
* @private
* Timestamp of when the prompt will timeout.
*/
const PERSISTED_EXPIRES = 'expires';

0 comments on commit ec780e5

Please sign in to comment.