botbuilder-dialogs
Version:
A dialog stack based conversation manager for Microsoft BotBuilder.
615 lines • 24.1 kB
JavaScript
/**
* @module botbuilder-dialogs
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DialogStateManager = void 0;
const componentMemoryScopes_1 = require("./componentMemoryScopes");
const componentPathResolvers_1 = require("./componentPathResolvers");
const botbuilder_core_1 = require("botbuilder-core");
const dialogPath_1 = require("./dialogPath");
const dialogsComponentRegistration_1 = require("../dialogsComponentRegistration");
const PATH_TRACKER = 'dialog._tracker.paths';
const DIALOG_STATE_MANAGER_CONFIGURATION = 'DialogStateManagerConfiguration';
/**
* The DialogStateManager manages memory scopes and path resolvers.
*
* @remarks
* MemoryScopes are named root level objects, which can exist either in the dialog context or off
* of turn state. Path resolvers allow for shortcut behavior for mapping things like
* $foo -> dialog.foo
*/
class DialogStateManager {
/**
* Initializes a new instance of the [DialogStateManager](xref:botbuilder-dialogs.DialogStateManager) class.
*
* @param dc The dialog context for the current turn of the conversation.
* @param configuration Configuration for the dialog state manager.
*/
constructor(dc, configuration) {
var _a, _b;
botbuilder_core_1.ComponentRegistration.add(new dialogsComponentRegistration_1.DialogsComponentRegistration());
this.dialogContext = dc;
this.configuration = configuration !== null && configuration !== void 0 ? configuration : dc.context.turnState.get(DIALOG_STATE_MANAGER_CONFIGURATION);
if (!this.configuration) {
this.configuration = { memoryScopes: [], pathResolvers: [] };
// get all of the component memory scopes.
botbuilder_core_1.ComponentRegistration.components
.filter((component) => componentMemoryScopes_1.isComponentMemoryScopes(component))
.forEach((component) => {
this.configuration.memoryScopes.push(...component.getMemoryScopes());
});
// merge in turn state memory scopes
const memoryScopes = (_a = dc.context.turnState.get('memoryScopes')) !== null && _a !== void 0 ? _a : [];
this.configuration.memoryScopes.push(...memoryScopes);
// get all of the component path resolvers.
botbuilder_core_1.ComponentRegistration.components
.filter((component) => componentPathResolvers_1.isComponentPathResolvers(component))
.forEach((component) => {
this.configuration.pathResolvers.push(...component.getPathResolvers());
});
// merge in turn state ones path resolvers
const pathResolvers = (_b = dc.context.turnState.get('pathResolvers')) !== null && _b !== void 0 ? _b : [];
this.configuration.pathResolvers.push(...pathResolvers);
// cache for any other new dialogStateManager instances in this turn
dc.context.turnState.set(DIALOG_STATE_MANAGER_CONFIGURATION, this.configuration);
}
}
/**
* Get the value from memory using path expression.
*
* @remarks
* This always returns a CLONE of the memory, any modifications to the result will not affect memory.
* @template T The value type to return.
* @param pathExpression Path expression to use.
* @param defaultValue (Optional) default value to use if the path isn't found. May be a function that returns the default value to use.
* @returns The found value or undefined if not found and no `defaultValue` specified.
*/
getValue(pathExpression, defaultValue) {
function returnDefault() {
return typeof defaultValue == 'function' ? defaultValue() : defaultValue;
}
// Get path segments
const segments = this.parsePath(this.transformPath(pathExpression));
if (segments.length < 1) {
return returnDefault();
}
// Get memory scope to search over
const scope = this.getMemoryScope(segments[0].toString());
if (scope == undefined) {
throw new Error(`DialogStateManager.getValue: a scope of '${segments[0]}' wasn't found.`);
}
// Search over path
const memory = this.resolveSegments(scope.getMemory(this.dialogContext), segments, false);
// Return default value if nothing found
return memory != undefined ? memory : returnDefault();
}
/**
* Set memory to value.
*
* @param pathExpression Path to memory.
* @param value Value to set.
*/
setValue(pathExpression, value) {
// Get path segments
const tpath = this.transformPath(pathExpression);
const segments = this.parsePath(tpath);
if (segments.length < 1) {
throw new Error("DialogStateManager.setValue: path wasn't specified.");
}
// Track changes
this.trackChange(tpath);
// Get memory scope to update
const scope = this.getMemoryScope(segments[0].toString());
if (scope == undefined) {
throw new Error(`DialogStateManager.setValue: a scope of '${segments[0]}' wasn't found.`);
}
// Update memory
if (segments.length > 1) {
// Find value up to last key
// - Missing paths will be populated as needed
let memory = scope.getMemory(this.dialogContext);
memory = this.resolveSegments(memory, segments, true);
// Update value
let key = segments[segments.length - 1];
if (key === 'first()') {
key = 0;
}
if (typeof key == 'number' && Array.isArray(memory)) {
// Only allow positive indexes
if (key < 0) {
throw new Error(`DialogStateManager.setValue: unable to update value for '${pathExpression}'. Negative indexes aren't allowed.`);
}
// Expand array as needed and update array
const l = key + 1;
while (memory.length < l) {
memory.push(undefined);
}
memory[key] = value;
}
else if (typeof key == 'string' &&
key.length > 0 &&
typeof memory == 'object' &&
!Array.isArray(memory)) {
// Find key to use and update object
key = this.findObjectKey(memory, key) || key;
memory[key] = value;
}
else {
throw new Error(`DialogStateManager.setValue: unable to update value for '${pathExpression}'.`);
}
}
else {
// Just update memory scope
scope.setMemory(this.dialogContext, value);
}
}
/**
* Delete property from memory
*
* @param pathExpression The leaf property to remove.
*/
deleteValue(pathExpression) {
// Get path segments
const tpath = this.transformPath(pathExpression);
const segments = this.parsePath(tpath);
if (segments.length < 2) {
throw new Error(`DialogStateManager.deleteValue: invalid path of '${pathExpression}'.`);
}
// Track change
this.trackChange(tpath);
// Get memory scope to update
const scope = this.getMemoryScope(segments[0].toString());
if (scope == undefined) {
throw new Error(`DialogStateManager.deleteValue: a scope of '${segments[0]}' wasn't found.`);
}
// Find value up to last key
const key = segments.pop();
const memory = this.resolveSegments(scope.getMemory(this.dialogContext), segments, false);
// Update value
if (typeof key == 'number' && Array.isArray(memory)) {
if (key < memory.length) {
memory.splice(key, 1);
}
}
else if (typeof key == 'string' && key.length > 0 && typeof memory == 'object' && !Array.isArray(memory)) {
const found = this.findObjectKey(memory, key);
if (found) {
delete memory[found];
}
}
}
/**
* Ensures that all memory scopes have been loaded for the current turn.
*
* @remarks
* This should be called at the beginning of the turn.
*/
loadAllScopes() {
return __awaiter(this, void 0, void 0, function* () {
const scopes = this.configuration.memoryScopes;
for (let i = 0; i < scopes.length; i++) {
yield scopes[i].load(this.dialogContext);
}
});
}
/**
* Saves any changes made to memory scopes.
*
* @remarks
* This should be called at the end of the turn.
*/
saveAllChanges() {
return __awaiter(this, void 0, void 0, function* () {
const scopes = this.configuration.memoryScopes;
for (let i = 0; i < scopes.length; i++) {
yield scopes[i].saveChanges(this.dialogContext);
}
});
}
/**
* Deletes all of the backing memory for a given scope.
*
* @param name Name of the scope.
*/
deleteScopesMemory(name) {
return __awaiter(this, void 0, void 0, function* () {
name = name.toLowerCase();
const scopes = this.configuration.memoryScopes;
for (let i = 0; i < scopes.length; i++) {
const scope = scopes[i];
if (scope.name.toLowerCase() == name) {
yield scope.delete(this.dialogContext);
break;
}
}
});
}
/**
* Normalizes the path segments of a passed in path.
*
* @remarks
* A path of `profile.address[0]` will be normalized to `profile.address.0`.
* @param pathExpression The path to normalize.
* @param allowNestedPaths Optional. If `false` then detection of a nested path will cause an empty path to be returned. Defaults to 'true'.
* @returns The normalized path.
*/
parsePath(pathExpression, allowNestedPaths = true) {
// Expand path segments
let segment = '';
let depth = 0;
let quote = '';
const output = [];
for (let i = 0; i < pathExpression.length; i++) {
const c = pathExpression[i];
if (depth > 0) {
// We're in a set of brackets
if (quote.length) {
// We're in a string
switch (c) {
case '\\':
// Escape code detected
i++;
segment += pathExpression[i];
break;
default:
segment += c;
if (c == quote) {
quote = '';
}
break;
}
}
else {
// We're in a bracket
switch (c) {
case '[':
depth++;
segment += c;
break;
case ']':
depth--;
if (depth > 0) {
segment += c;
}
break;
case "'":
case '"':
quote = c;
segment += c;
break;
default:
segment += c;
break;
}
// Are we out of the brackets
if (depth == 0) {
if (isQuoted(segment)) {
// Quoted segment
output.push(segment.length > 2 ? segment.substr(1, segment.length - 2) : '');
}
else if (isIndex(segment)) {
// Array index
output.push(parseInt(segment));
}
else if (allowNestedPaths) {
// Resolve nested value
const val = this.getValue(segment);
const t = typeof val;
output.push(t == 'string' || t == 'number' ? val : '');
}
else {
// Abort parsing and return empty path (used for change tracking.)
return [];
}
segment = '';
}
}
}
else {
// We're parsing the outer path
switch (c) {
case '[':
if (segment.length > 0) {
output.push(segment);
segment = '';
}
depth++;
break;
case '.':
if (segment.length > 0) {
output.push(segment);
segment = '';
}
else if (i == 0 || i == pathExpression.length - 1) {
// Special case a "." at beginning or end of path
output.push('');
}
else if (pathExpression[i - 1] == '.') {
// Special case ".."
output.push('');
}
break;
default:
if (i > 1 && pathExpression[i - 1] == '.' && c == '$') {
segment += c; // x.$foo should be valid
}
else if (isValidPathChar(c)) {
segment += c;
}
else {
throw new Error(`DialogStateManager.normalizePath: Invalid path detected - ${pathExpression}`);
}
break;
}
}
}
if (depth > 0) {
throw new Error(`DialogStateManager.normalizePath: Invalid path detected - ${pathExpression}`);
}
else if (segment.length > 0) {
output.push(segment);
}
return output;
}
/**
* Transform the path using the registered path transformers.
*
* @param pathExpression The path to transform.
* @returns The transformed path.
*/
transformPath(pathExpression) {
// Run path through registered resolvers.
const resolvers = this.configuration.pathResolvers;
for (let i = 0; i < resolvers.length; i++) {
pathExpression = resolvers[i].transformPath(pathExpression);
}
return pathExpression;
}
/**
* Gets all memory scopes suitable for logging.
*
* @returns Object which represents all memory scopes.
*/
getMemorySnapshot() {
const output = {};
this.configuration.memoryScopes.forEach((scope) => {
if (scope.includeInSnapshot) {
output[scope.name] = scope.getMemory(this.dialogContext);
}
});
return output;
}
/**
* Track when specific paths are changed.
*
* @param paths Paths to track.
* @returns Normalized paths to pass to [anyPathChanged()](#anypathchanged).
*/
trackPaths(paths) {
const allPaths = [];
paths.forEach((path) => {
const tpath = this.transformPath(path);
const segments = this.parsePath(tpath, false);
if (segments.length > 0 && (segments.length == 1 || !segments[1].toString().startsWith('_'))) {
// Normalize path and initialize change tracker
const npath = segments.join('_').toLowerCase();
this.setValue(`${PATH_TRACKER}.${npath}`, 0);
// Return normalized path
allPaths.push(npath);
}
});
return allPaths;
}
/**
* Check to see if any path has changed since watermark.
*
* @param counter Time counter to compare to.
* @param paths Paths from [trackPaths()](#trackpaths) to check.
* @returns True if any path has changed since counter.
*/
anyPathChanged(counter, paths) {
let found = false;
if (paths) {
for (let i = 0; i < paths.length; i++) {
if (this.getValue(`${PATH_TRACKER}.${paths[i]}`, 0) > counter) {
found = true;
break;
}
}
}
return found;
}
/**
* @private
* @param path Track path to change.
*/
trackChange(path) {
// Normalize path and scan for any matches or children that match.
// - We're appending an extra '_' so that we can do substring matches and
// avoid any false positives.
let counter = undefined;
const npath = this.parsePath(path, false).join('_') + '_';
const tracking = this.getValue(PATH_TRACKER) || {};
for (const key in tracking) {
if (`${key}_`.startsWith(npath)) {
// Populate counter on first use
if (counter == undefined) {
counter = this.getValue(dialogPath_1.DialogPath.eventCounter);
}
// Update tracking watermark
this.setValue(`${PATH_TRACKER}.${key}`, counter);
}
}
}
/**
* @private
* @param memory Object memory to resolve.
* @param segments Segments of the memory to resolve.
* @param assignment Optional.
* @returns The value of the memory segment.
*/
resolveSegments(memory, segments, assignment) {
let value = memory;
const l = assignment ? segments.length - 1 : segments.length;
for (let i = 1; i < l && value != undefined; i++) {
const key = segments[i];
if (typeof key == 'number') {
// Key is an array index
if (Array.isArray(value)) {
value = value[key];
}
else {
value = undefined;
}
}
else if (key === 'first()') {
// Special case returning the first entity in an array of entities.
if (Array.isArray(value) && value.length > 0) {
value = value[0];
if (Array.isArray(value)) {
// Nested array detected
if (value.length > 0) {
value = value[0];
}
else {
value = undefined;
}
}
}
else {
value = undefined;
}
}
else if (typeof key == 'string' && key.length > 0) {
// Key is an object index
if (typeof value == 'object' && !Array.isArray(value)) {
// Case-insensitive search for prop
let found = this.findObjectKey(value, key);
// Ensure path exists as needed
if (assignment) {
const nextKey = segments[i + 1];
if (typeof nextKey == 'number' || nextKey === 'first()') {
// Ensure prop contains an array
if (found) {
if (value[found] == undefined) {
value[found] = [];
}
}
else {
found = key;
value[found] = [];
}
}
else if (typeof nextKey == 'string' && nextKey.length > 0) {
// Ensure prop contains an object
if (found) {
if (value[found] == undefined) {
value[found] = {};
}
}
else {
found = key;
value[found] = {};
}
}
else {
// We can't determine type so return undefined
found = undefined;
}
}
value = found ? value[found] : undefined;
}
else {
value = undefined;
}
}
else {
// Key is missing
value = undefined;
}
}
return value;
}
/**
* @private
*/
findObjectKey(obj, key) {
const k = key.toLowerCase();
for (const prop in obj) {
if (prop.toLowerCase() == k) {
return prop;
}
}
return undefined;
}
/**
* @private
* Gets [MemoryScope](xref:botbuilder-dialogs.MemoryScope) by name.
* @param name Name of scope.
* @returns The [MemoryScope](xref:botbuilder-dialogs.MemoryScope).
*/
getMemoryScope(name) {
const key = name.toLowerCase();
const scopes = this.configuration.memoryScopes;
for (let i = 0; i < scopes.length; i++) {
const scope = scopes[i];
if (scope.name.toLowerCase() == key) {
return scope;
}
}
return undefined;
}
/**
* Gets the version number.
*
* @returns A string with the version number.
*/
version() {
return '0';
}
}
exports.DialogStateManager = DialogStateManager;
/**
* @private
*/
function isIndex(segment) {
const digits = '0123456789';
for (let i = 0; i < segment.length; i++) {
const c = segment[i];
if (digits.indexOf(c) < 0) {
// Check for negative sign
if (c != '-' || i > 0 || segment.length < 2) {
return false;
}
}
}
return segment.length > 0;
}
/**
* @private
*/
function isQuoted(segment) {
return ((segment.length > 1 && segment.startsWith("'") && segment.endsWith("'")) ||
(segment.startsWith('"') && segment.endsWith('"')));
}
/**
* @private
*/
function isValidPathChar(c) {
return '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-()'.indexOf(c) >= 0;
}
//# sourceMappingURL=dialogStateManager.js.map
;