@richardmcquiston01/chat-and-react-core 0.8.0 → 0.9.0

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.
package/dist/index.cjs CHANGED
@@ -231,6 +231,10 @@ var SessionManager = class {
231
231
  return null;
232
232
  }
233
233
  }
234
+ clearInput(questionId) {
235
+ if (!this.available) return;
236
+ this.storage.removeItem(keyInput(this.sessionId, questionId));
237
+ }
234
238
  clearAll() {
235
239
  if (!this.available) return;
236
240
  const progressKey = keyProgress(this.sessionId);
@@ -723,6 +727,12 @@ var AdapterSessionManager = class _AdapterSessionManager {
723
727
  loadInput(questionId) {
724
728
  return this.record.inputs[questionId] ?? null;
725
729
  }
730
+ clearInput(questionId) {
731
+ if (!(questionId in this.record.inputs)) return;
732
+ delete this.record.inputs[questionId];
733
+ this.dirtyInputs.add(questionId);
734
+ this.schedulePersist();
735
+ }
726
736
  clearAll() {
727
737
  this.record = emptyRecord();
728
738
  this.dirtyInputs.clear();
@@ -824,7 +834,11 @@ var AdapterSessionManager = class _AdapterSessionManager {
824
834
  const inputs = { ...remote.inputs };
825
835
  for (const questionId of this.dirtyInputs) {
826
836
  const mine = this.record.inputs[questionId];
827
- if (mine !== void 0) inputs[questionId] = mine;
837
+ if (mine !== void 0) {
838
+ inputs[questionId] = mine;
839
+ } else {
840
+ delete inputs[questionId];
841
+ }
828
842
  }
829
843
  this.record = {
830
844
  progress: this.dirtyProgress ? this.record.progress : remote.progress,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/adapters/browser-event-adapter.ts","../src/adapters/console-error-adapter.ts","../src/adapters/identity-input-adapter.ts","../src/rule-engine.ts","../src/schema-parser.ts","../src/session-manager.ts","../src/chat-controller.ts","../src/storage/adapter-session-manager.ts"],"sourcesContent":["export type {\n StorageAdapter,\n StoredValue,\n StoreErrorCode,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\n// The browser storage adapters live in the storage-adapters package; re-exported here so\n// they can be imported from core. Server-side adapters (SQLite, files) are in the adapters\n// package.\nexport type {\n LocalStorageAdapterOptions,\n SessionStorageAdapterOptions,\n StorageChangeEvent,\n StorageEventTarget,\n WebStorageAdapterOptions,\n WebStorageLike,\n WebStorageName,\n} from '@richardmcquiston01/storage-adapters/browser';\nexport {\n localStorageAdapter,\n sessionStorageAdapter,\n} from '@richardmcquiston01/storage-adapters/browser';\nexport type { IndexedDbAdapterOptions } from '@richardmcquiston01/storage-adapters/indexeddb';\nexport { indexedDbAdapter } from '@richardmcquiston01/storage-adapters/indexeddb';\nexport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nexport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nexport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nexport { ChatController } from './chat-controller.js';\nexport { RuleEngine } from './rule-engine.js';\nexport { parseSchema } from './schema-parser.js';\nexport type { SessionProgress, SessionStore, StorageBackend } from './session-manager.js';\nexport { SessionManager } from './session-manager.js';\nexport type { AdapterSessionManagerOptions } from './storage/adapter-session-manager.js';\nexport { AdapterSessionManager } from './storage/adapter-session-manager.js';\nexport * from './types.js';\n","import type { ChatEvent, OutputAdapter } from '../types.js';\n\nexport class BrowserEventAdapter implements OutputAdapter {\n emit(event: ChatEvent): void {\n if (typeof document === 'undefined') return;\n const name = event.type === 'page:submit' ? 'chat-page-submit' : 'chat-form-complete';\n document.dispatchEvent(new CustomEvent(name, { detail: event, bubbles: true }));\n }\n}\n","import {\n ConsoleErrorAdapter as BaseConsoleErrorAdapter,\n type ConsoleErrorOptions,\n} from '@richardmcquiston01/error-log-adapter';\nimport type { ChatErrorCode } from '../types.js';\n\n/** Writes errors to `console.error`, prefixed with `[chat-and-react]` unless overridden. */\nexport class ConsoleErrorAdapter extends BaseConsoleErrorAdapter<ChatErrorCode> {\n constructor(options: ConsoleErrorOptions = {}) {\n super({ prefix: '[chat-and-react]', ...options });\n }\n}\n","import type { InputAdapter } from '../types.js';\n\nexport class IdentityInputAdapter implements InputAdapter {\n transform(raw: unknown): unknown {\n return raw;\n }\n}\n","import jsonLogic from 'json-logic-js';\nimport type { AnswerMap, JsonLogicRule } from './types.js';\n\n/**\n * Evaluates JSON Logic rules against provided answers.\n * Wraps json-logic-js to provide type-safe rule evaluation.\n */\nexport class RuleEngine {\n /**\n * Evaluates a JSON Logic rule against an answer map.\n * @param rule The JSON Logic rule to evaluate\n * @param answers The answer map to evaluate the rule against\n * @returns The boolean result of the rule evaluation\n * @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails\n */\n evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean {\n try {\n // json-logic-js apply() returns any; cast to boolean\n return Boolean(\n (jsonLogic as { apply: (rule: unknown, data: unknown) => unknown }).apply(rule, answers),\n );\n } catch (cause) {\n throw { code: 'RULE_EVAL_FAILED', message: 'Rule evaluation failed', cause };\n }\n }\n}\n","import type { ChatPage, ChatSchema } from './types.js';\n\nconst VALID_INPUTS = new Set(['text', 'textarea', 'dropdown', 'radio', 'checkbox']);\nconst FORM_COMPLETED = '**FORM_COMPLETED**';\n\n/**\n * Parses and validates an unknown input as a {@link ChatSchema}.\n *\n * @param raw - The raw input to validate.\n * @returns The typed {@link ChatSchema} if valid.\n * @throws `{ code: 'SCHEMA_INVALID', message: string }` on any validation failure.\n */\nexport function parseSchema(raw: unknown): ChatSchema {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Schema must be a non-null object');\n }\n\n const obj = raw as Record<string, unknown>;\n\n if (obj['x-chat-version'] !== '1') {\n throw makeError('x-chat-version must be \"1\"');\n }\n\n if (!Array.isArray(obj['x-chat-pages'])) {\n throw makeError('x-chat-pages must be an array');\n }\n\n const pages = obj['x-chat-pages'] as unknown[];\n const rules = (\n typeof obj['x-chat-rules'] === 'object' && obj['x-chat-rules'] !== null\n ? obj['x-chat-rules']\n : {}\n ) as Record<string, unknown>;\n\n const pageIds = new Set<string>();\n const questionIds = new Set<string>();\n\n for (const page of pages) {\n validatePage(page, pageIds, questionIds, rules);\n }\n\n // Second pass: validate branching destinations now that all page ids are known\n for (const page of pages) {\n const p = page as ChatPage;\n for (const rule of p.branching) {\n if (rule.destination_id !== FORM_COMPLETED && !pageIds.has(rule.destination_id)) {\n throw makeError(\n `branching destination_id \"${rule.destination_id}\" does not reference a known page id`,\n );\n }\n if (rule.type === 'if' && !questionIds.has(rule.form_element)) {\n throw makeError(\n `branching form_element \"${rule.form_element}\" does not reference a known question id`,\n );\n }\n }\n }\n\n return raw as ChatSchema;\n}\n\n/**\n * Validates a single page object, collecting its id and question ids.\n *\n * @param raw - The raw page value.\n * @param pageIds - Set of already-seen page ids (mutated in place).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validatePage(\n raw: unknown,\n pageIds: Set<string>,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each page must be a non-null object');\n }\n const page = raw as Record<string, unknown>;\n\n if (typeof page['id'] !== 'string' || !page['id']) {\n throw makeError('Page id must be a non-empty string');\n }\n if (pageIds.has(page['id'] as string)) {\n throw makeError(`Duplicate page id \"${page['id']}\"`);\n }\n pageIds.add(page['id'] as string);\n\n if (!Array.isArray(page['questions'])) {\n throw makeError(`Page \"${page['id']}\" must have a questions array`);\n }\n\n const branching = page['branching'];\n if (!Array.isArray(branching) || (branching as unknown[]).length === 0) {\n throw makeError(`Page \"${page['id']}\" must have a non-empty branching array`);\n }\n\n const lastRule = (branching as unknown[])[branching.length - 1] as Record<string, unknown>;\n if (lastRule['type'] !== 'always') {\n throw makeError(`Page \"${page['id']}\" branching must end with a rule of type \"always\"`);\n }\n\n for (const q of page['questions'] as unknown[]) {\n validateQuestion(q, page['id'] as string, questionIds, rules);\n }\n}\n\n/**\n * Validates a single question object, collecting its id.\n *\n * @param raw - The raw question value.\n * @param pageId - The id of the containing page (for error messages).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validateQuestion(\n raw: unknown,\n pageId: string,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each question must be a non-null object');\n }\n const q = raw as Record<string, unknown>;\n\n if (typeof q['id'] !== 'string' || !q['id']) {\n throw makeError(`In page \"${pageId}\": question id must be a non-empty string`);\n }\n if (questionIds.has(q['id'] as string)) {\n throw makeError(`Duplicate question id \"${q['id']}\"`);\n }\n questionIds.add(q['id'] as string);\n\n if (!VALID_INPUTS.has(q['x-chat-input'] as string)) {\n throw makeError(\n `Question \"${q['id']}\" has invalid x-chat-input \"${q['x-chat-input']}\". ` +\n `Must be one of: text, textarea, dropdown, radio, checkbox`,\n );\n }\n\n const condition = q['x-chat-condition'];\n if (condition && typeof condition === 'object' && !Array.isArray(condition)) {\n const c = condition as Record<string, unknown>;\n if ('ref' in c && typeof c['ref'] === 'string') {\n if (!(c['ref'] in rules)) {\n throw makeError(`Question \"${q['id']}\" x-chat-condition refs unknown rule \"${c['ref']}\"`);\n }\n }\n }\n}\n\n/**\n * Creates a schema validation error object.\n *\n * @param message - Human-readable description of the validation failure.\n * @returns An error object with `code: 'SCHEMA_INVALID'`.\n */\nfunction makeError(message: string): { code: 'SCHEMA_INVALID'; message: string } {\n return { code: 'SCHEMA_INVALID', message };\n}\n","// packages/core/src/session-manager.ts\nimport type { AnswerMap } from './types.js';\n\nexport interface StorageBackend {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n key(index: number): string | null;\n readonly length: number;\n}\n\nexport interface SessionProgress {\n currentPageId: string;\n answers: AnswerMap;\n}\n\nconst KEY_SESSION = 'car:session-id';\nconst keyProgress = (id: string) => `car:progress:${id}`;\nconst keyInput = (sessionId: string, questionId: string) => `car:input:${sessionId}:${questionId}`;\n\n/**\n * What `ChatController` needs from session storage. `SessionManager` keeps it in a\n * synchronous Web Storage object; `AdapterSessionManager` keeps it in a `StorageAdapter`.\n */\nexport interface SessionStore {\n readonly isStorageAvailable: boolean;\n getSessionId(): string;\n saveProgress(currentPageId: string, answers: AnswerMap): void;\n loadProgress(): SessionProgress | null;\n saveInput(questionId: string, value: string | string[]): void;\n loadInput(questionId: string): string | string[] | null;\n clearAll(): void;\n /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */\n dispose?(): void;\n}\n\nexport class SessionManager implements SessionStore {\n private readonly sessionId: string;\n private readonly available: boolean;\n\n constructor(private readonly storage: StorageBackend = globalThis.localStorage) {\n this.available = this.checkStorage();\n this.sessionId = this.loadOrCreateSessionId();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n if (!this.available) return;\n this.storage.setItem(keyProgress(this.sessionId), JSON.stringify({ currentPageId, answers }));\n }\n\n loadProgress(): SessionProgress | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyProgress(this.sessionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as SessionProgress;\n } catch {\n return null;\n }\n }\n\n saveInput(questionId: string, value: string | string[]): void {\n if (!this.available) return;\n this.storage.setItem(keyInput(this.sessionId, questionId), JSON.stringify(value));\n }\n\n loadInput(questionId: string): string | string[] | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyInput(this.sessionId, questionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as string | string[];\n } catch {\n return null;\n }\n }\n\n clearAll(): void {\n if (!this.available) return;\n const progressKey = keyProgress(this.sessionId);\n const inputPrefix = `car:input:${this.sessionId}:`;\n const keysToRemove: string[] = [progressKey];\n\n for (let i = 0; i < this.storage.length; i++) {\n const k = this.storage.key(i);\n if (k?.startsWith(inputPrefix)) keysToRemove.push(k);\n }\n\n for (const k of keysToRemove) {\n this.storage.removeItem(k);\n }\n }\n\n private loadOrCreateSessionId(): string {\n if (!this.available) return crypto.randomUUID();\n const existing = this.storage.getItem(KEY_SESSION);\n if (existing) return existing;\n const id = crypto.randomUUID();\n this.storage.setItem(KEY_SESSION, id);\n return id;\n }\n\n private checkStorage(): boolean {\n try {\n const probe = '__car_probe__';\n this.storage.setItem(probe, '1');\n this.storage.removeItem(probe);\n return true;\n } catch {\n return false;\n }\n }\n}\n","// packages/core/src/chat-controller.ts\n\nimport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nimport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nimport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nimport { RuleEngine } from './rule-engine.js';\nimport { parseSchema } from './schema-parser.js';\nimport { SessionManager, type SessionStore } from './session-manager.js';\nimport type {\n AnswerMap,\n ChatControllerConfig,\n ChatError,\n ChatEvent,\n ChatOptions,\n ChatPage,\n ChatQuestion,\n ChatSchema,\n ErrorLogResult,\n RouteEntry,\n SubmitOutcome,\n} from './types.js';\n\ntype EventName = 'page:submit' | 'form:complete';\ntype Listener = (event: ChatEvent) => void;\n\nconst DEFAULT_OPTIONS: ChatOptions = {\n userResumable: true,\n localStorage: true,\n autoComplete: true,\n allowEdit: false,\n autoHideQuestions: 0,\n autoHideAnswers: 0,\n hideAnimation: 'fade',\n typingSpeed: 0,\n thinkingDelay: 0,\n thinkingText: '',\n showContinueIcon: true,\n};\n\n/** Copies an object without its undefined values, so they cannot hide a default. */\nfunction stripUndefined<T extends object>(source: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(source).filter(([, value]) => value !== undefined),\n ) as Partial<T>;\n}\n\n/**\n * Central orchestrator for the chat-and-react form flow.\n * Drives page navigation, branching, session persistence, and event emission.\n */\nexport class ChatController {\n private schema: ChatSchema | null = null;\n private options: ChatOptions = { ...DEFAULT_OPTIONS };\n private readonly config: Required<ChatControllerConfig>;\n private readonly session: SessionStore;\n private readonly ruleEngine = new RuleEngine();\n private currentPage: ChatPage | null = null;\n private allAnswers: AnswerMap = {};\n private visitIndex = 0;\n /** Pages submitted in this session, in visit order. Used for editing earlier answers. */\n private route: RouteEntry[] = [];\n /** Index in `route` of the page being edited, or null when not editing. */\n private editIndex: number | null = null;\n /** The page the user was on before they started editing an earlier one. */\n private resumePageId: string | null = null;\n private readonly listeners = new Map<EventName, Set<Listener>>();\n\n /**\n * @param config - Controller configuration including schema and optional adapters.\n * @param sessionManager - Optional session manager for test injection.\n */\n constructor(config: ChatControllerConfig, sessionManager?: SessionStore) {\n this.config = {\n inputAdapter: config.inputAdapter ?? new IdentityInputAdapter(),\n outputAdapter: config.outputAdapter ?? new BrowserEventAdapter(),\n errorAdapter: config.errorAdapter ?? new ConsoleErrorAdapter(),\n schema: config.schema,\n options: config.options ?? {},\n };\n this.session = sessionManager ?? new SessionManager();\n }\n\n /**\n * Initialises the controller: parses the schema, resolves options,\n * restores any saved session, and positions to the first page.\n */\n start(): void {\n let raw: unknown;\n try {\n raw = this.config.inputAdapter.transform(this.config.schema);\n } catch (cause) {\n this.handleFatal({ code: 'ADAPTER_ERROR', message: 'InputAdapter.transform() threw', cause });\n }\n\n let schema: ChatSchema;\n try {\n schema = parseSchema(raw);\n } catch (e) {\n this.handleFatal(e as ChatError);\n }\n this.schema = schema;\n\n if (!this.session.isStorageAvailable) {\n this.logError({\n code: 'STORAGE_UNAVAILABLE',\n message: 'localStorage unavailable; running in memory-only mode',\n });\n }\n\n // Defaults, then the schema, then overrides passed in by the host (for\n // example element attributes).\n const schemaOpts = schema['x-chat-options'] ?? {};\n this.options = {\n ...DEFAULT_OPTIONS,\n ...stripUndefined(schemaOpts),\n ...stripUndefined(this.config.options),\n };\n\n if (this.options.userResumable) {\n const saved = this.session.loadProgress();\n if (saved) {\n const page = schema['x-chat-pages'].find((p) => p.id === saved.currentPageId);\n if (page) {\n this.allAnswers = saved.answers;\n this.currentPage = page;\n return;\n }\n }\n }\n\n this.currentPage = schema['x-chat-pages'][0];\n }\n\n /**\n * Returns the page currently being presented to the user, or null if complete.\n */\n getCurrentPage(): ChatPage | null {\n return this.currentPage;\n }\n\n /**\n * Returns the subset of questions on the current page that pass their\n * `x-chat-condition` rule (if any). Questions without a condition are always visible.\n */\n getVisibleQuestions(): ChatQuestion[] {\n if (!this.currentPage) return [];\n return this.currentPage.questions.filter((q) => this.isQuestionVisible(q));\n }\n\n /**\n * Returns the resolved options for the current session (schema options merged with defaults).\n */\n getOptions(): ChatOptions {\n return { ...this.options };\n }\n\n /**\n * Returns the pages the user has submitted in this session, in visit order.\n * A session restored from storage starts with an empty route.\n */\n getRoute(): RouteEntry[] {\n return this.route.map((entry) => ({ pageId: entry.pageId, answers: { ...entry.answers } }));\n }\n\n /**\n * Returns every answer given so far, for pre-filling inputs.\n */\n getAnswers(): AnswerMap {\n return { ...this.allAnswers };\n }\n\n /**\n * Re-opens a page the user already submitted so its answers can be changed.\n * Call `submitPage` afterwards to save the new answers. Not available once the\n * form is complete.\n *\n * @param routeIndex - Position of the page in `getRoute()`.\n * @returns true when the page was re-opened.\n */\n reopenPage(routeIndex: number): boolean {\n if (!this.schema || !this.currentPage) return false;\n const entry = this.route[routeIndex];\n if (!entry) return false;\n const page = this.schema['x-chat-pages'].find((p) => p.id === entry.pageId);\n if (!page) return false;\n\n // Remember where the user was, but only the first time: editing another\n // earlier page before finishing this edit must still return to that page.\n if (this.editIndex === null) this.resumePageId = this.currentPage.id;\n this.editIndex = routeIndex;\n this.currentPage = page;\n return true;\n }\n\n /**\n * Submits the answers for the current page and advances the session.\n * - Accumulates answers into `allAnswers`.\n * - Emits `page:submit` (suppressed when `userResumable` is false).\n * - Resolves branching to find the next page.\n * - Emits `form:complete` and clears session when `**FORM_COMPLETED**` is reached.\n *\n * When the page was re-opened with `reopenPage`, the new answers replace the\n * old ones and `page:submit` is emitted with `edited: true`. If the page's\n * branching still leads to the same next page, the form returns to the page the\n * user was on. If it leads somewhere else, the pages that followed it are\n * dropped, along with their answers, and the form continues from the new page.\n *\n * @param pageAnswers - Answers collected for the current page.\n */\n submitPage(pageAnswers: AnswerMap): SubmitOutcome {\n if (!this.currentPage || !this.schema) return 'advanced';\n\n const pageId = this.currentPage.id;\n const currentVisitIndex = this.visitIndex;\n const editedIndex = this.editIndex;\n const isEdit = editedIndex !== null;\n\n let outcome: SubmitOutcome = 'advanced';\n let nextPageId: string;\n let resumePageId: string | null = null;\n\n if (isEdit) {\n const entry = this.route[editedIndex];\n // Replace the old answers for this page, including any question that is\n // no longer visible after the change.\n const remaining = { ...this.allAnswers };\n for (const questionId of Object.keys(entry.answers)) delete remaining[questionId];\n this.allAnswers = { ...remaining, ...pageAnswers };\n entry.answers = { ...pageAnswers };\n\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n const expectedNextPageId = this.route[editedIndex + 1]?.pageId ?? this.resumePageId;\n\n if (nextPageId === expectedNextPageId) {\n outcome = 'edit-resumed';\n resumePageId = this.resumePageId;\n } else {\n outcome = 'edit-rerouted';\n // The route changed: forget the pages that followed and their answers.\n for (const later of this.route.slice(editedIndex + 1)) {\n for (const questionId of Object.keys(later.answers)) delete this.allAnswers[questionId];\n }\n this.route.length = editedIndex + 1;\n }\n this.editIndex = null;\n this.resumePageId = null;\n } else {\n this.allAnswers = { ...this.allAnswers, ...pageAnswers };\n this.route.push({ pageId, answers: { ...pageAnswers } });\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n }\n\n if (this.options.userResumable) {\n const progressPageId =\n outcome === 'edit-resumed' && resumePageId\n ? resumePageId\n : nextPageId === '**FORM_COMPLETED**'\n ? pageId\n : nextPageId;\n this.session.saveProgress(progressPageId, this.allAnswers);\n\n const submitEvent: ChatEvent = {\n type: 'page:submit',\n sessionId: this.session.getSessionId(),\n pageId,\n visitIndex: currentVisitIndex,\n answers: pageAnswers,\n ...(isEdit ? { edited: true } : {}),\n };\n this.safeEmit(submitEvent);\n this.emit('page:submit', submitEvent);\n }\n\n this.visitIndex++;\n\n if (outcome === 'edit-resumed' && resumePageId) {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === resumePageId) ?? null;\n return outcome;\n }\n\n if (nextPageId === '**FORM_COMPLETED**') {\n this.session.clearAll();\n const completeEvent: ChatEvent = {\n type: 'form:complete',\n sessionId: this.session.getSessionId(),\n allAnswers: this.allAnswers,\n };\n this.safeEmit(completeEvent);\n this.emit('form:complete', completeEvent);\n this.currentPage = null;\n } else {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === nextPageId) ?? null;\n }\n return outcome;\n }\n\n /**\n * Registers an event listener for the given event type.\n *\n * @param event - The event to listen for.\n * @param listener - The callback to invoke when the event fires.\n */\n on(event: EventName, listener: Listener): void {\n let listenersForEvent = this.listeners.get(event);\n if (!listenersForEvent) {\n listenersForEvent = new Set();\n this.listeners.set(event, listenersForEvent);\n }\n listenersForEvent.add(listener);\n }\n\n /**\n * Removes a previously registered event listener.\n *\n * @param event - The event type to deregister from.\n * @param listener - The specific listener to remove.\n */\n off(event: EventName, listener: Listener): void {\n this.listeners.get(event)?.delete(listener);\n }\n\n /**\n * Tears down the controller: removes all listeners and nulls internal state.\n */\n destroy(): void {\n this.session.dispose?.();\n this.listeners.clear();\n this.currentPage = null;\n this.schema = null;\n }\n\n /**\n * Evaluates whether a question should be shown based on its `x-chat-condition`.\n * Returns true when no condition is present. On evaluation failure, logs and returns false.\n */\n private isQuestionVisible(question: ChatQuestion): boolean {\n const condition = question['x-chat-condition'];\n if (!condition) return true;\n\n const rules = this.schema?.['x-chat-rules'] ?? {};\n let rule: Record<string, unknown>;\n\n if ('ref' in condition && typeof (condition as { ref?: unknown }).ref === 'string') {\n const ref = (condition as { ref: string }).ref;\n rule = rules[ref];\n if (!rule) return false;\n } else {\n rule = condition as Record<string, unknown>;\n }\n\n try {\n return this.ruleEngine.evaluate(rule, this.answersForRules());\n } catch (e) {\n this.logError(e as ChatError);\n return false;\n }\n }\n\n /**\n * The answers question conditions are checked against. While an earlier page is\n * being edited, that page's own answers are left out, so its questions are\n * shown exactly as they were the first time the page was asked.\n */\n private answersForRules(): AnswerMap {\n if (this.editIndex === null) return this.allAnswers;\n const rest = { ...this.allAnswers };\n for (const questionId of Object.keys(this.route[this.editIndex].answers))\n delete rest[questionId];\n return rest;\n }\n\n /**\n * Walks the branching rules top-to-bottom. Returns the first match's destination_id.\n * Falls back to `**FORM_COMPLETED**` if nothing matches (should not happen with valid schemas).\n */\n private resolveBranching(page: ChatPage, answers: AnswerMap): string {\n for (const rule of page.branching) {\n if (rule.type === 'always') return rule.destination_id;\n if (rule.type === 'if' && answers[rule.form_element] === rule.value) {\n return rule.destination_id;\n }\n }\n return '**FORM_COMPLETED**';\n }\n\n /**\n * Calls `outputAdapter.emit()` and swallows adapter errors (logging them instead).\n */\n private safeEmit(event: ChatEvent): void {\n try {\n this.config.outputAdapter.emit(event);\n } catch (cause) {\n this.logError({\n code: 'ADAPTER_ERROR',\n message: `OutputAdapter.emit() threw for ${event.type}`,\n cause,\n });\n }\n }\n\n /**\n * Notifies all registered listeners for the given event.\n */\n private emit(event: EventName, payload: ChatEvent): void {\n this.listeners.get(event)?.forEach((l) => {\n l(payload);\n });\n }\n\n /**\n * Logs the error via the error adapter and then throws it.\n * Used for unrecoverable errors (schema invalid, adapter failure on start).\n */\n private handleFatal(error: ChatError): never {\n this.logError(error);\n throw error;\n }\n\n /**\n * Sends the error to the error adapter without waiting for it. If the adapter\n * reports a failure, rejects, or throws, that is written to `console.error` so\n * the original error is never lost and the form keeps running.\n */\n private logError(entry: ChatError): void {\n const reportFailure = (reason: unknown): void => {\n console.error('[chat-and-react] ErrorLogAdapter failed to record an error:', reason, entry);\n };\n try {\n // `Promise.resolve` also tolerates older custom adapters that return nothing.\n void Promise.resolve(this.config.errorAdapter.log(entry)).then(\n (result: ErrorLogResult | undefined): void => {\n if (result?.ok === false) {\n reportFailure(result.error);\n }\n },\n reportFailure,\n );\n } catch (cause) {\n reportFailure(cause);\n }\n }\n}\n","import type {\n StorageAdapter,\n StoredValue,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\nimport type { SessionProgress, SessionStore } from '../session-manager.js';\nimport type { AnswerMap, ChatError } from '../types.js';\n\nconst KEY_SESSION = 'car:session-id';\nconst keyRecord = (sessionId: string): string => `car:session:${sessionId}`;\n\n/** After this many revision conflicts in a row, a save is abandoned and reported. */\nconst MAX_CONFLICT_RETRIES = 3;\n\ntype InputValue = string | string[];\n\n/** Everything saved for one session, stored under a single key. */\ninterface SessionRecord {\n progress: SessionProgress | null;\n inputs: Record<string, InputValue>;\n}\n\nexport interface AdapterSessionManagerOptions {\n /** Called when the adapter fails. The session keeps working from memory. */\n onError?: (error: ChatError) => void;\n /**\n * Called when saved progress changes because of a write made elsewhere, such as\n * another tab. `null` means the session was cleared there.\n */\n onExternalChange?: (progress: SessionProgress | null) => void;\n}\n\nconst emptyRecord = (): SessionRecord => ({ progress: null, inputs: {} });\n\nfunction parseRecord(raw: string): SessionRecord | null {\n try {\n const parsed = JSON.parse(raw) as Partial<SessionRecord>;\n return {\n progress: parsed.progress ?? null,\n inputs: parsed.inputs && typeof parsed.inputs === 'object' ? parsed.inputs : {},\n };\n } catch {\n return null;\n }\n}\n\n/**\n * A session store backed by a `StorageAdapter`. `ChatController` is synchronous, so\n * the saved session is loaded into memory by `create()`, reads come from memory, and\n * writes are saved in the background. Failures are reported through `onError`.\n *\n * Saves carry the revision they were based on. If another tab wrote first, the two\n * are merged: inputs per question, and progress only when this tab saved progress\n * itself, so a tab that only typed cannot overwrite another tab's progress.\n */\nexport class AdapterSessionManager implements SessionStore {\n private record: SessionRecord;\n private revision: string | undefined;\n private readonly dirtyInputs = new Set<string>();\n private dirtyProgress = false;\n private pending = false;\n private persisting: Promise<void> | null = null;\n private readonly signal: AbortSignal = new AbortController().signal;\n private unsubscribe: (() => void) | null = null;\n\n private constructor(\n private readonly adapter: StorageAdapter,\n private readonly sessionId: string,\n private readonly available: boolean,\n initial: StoredValue | null,\n private readonly options: AdapterSessionManagerOptions,\n ) {\n const parsed: SessionRecord | null = initial ? parseRecord(initial.value) : null;\n this.record = parsed ?? emptyRecord();\n this.revision = initial?.revision;\n if (initial && !parsed) {\n this.report(\n `Saved session ${sessionId} could not be read as JSON; starting with an empty session`,\n );\n }\n if (available) {\n this.unsubscribe =\n adapter.subscribe?.(keyRecord(sessionId), (stored) => this.applyRemote(stored)) ?? null;\n }\n }\n\n /** Loads (or creates) the session id and its saved record, then returns the manager. */\n static async create(\n adapter: StorageAdapter,\n options: AdapterSessionManagerOptions = {},\n ): Promise<AdapterSessionManager> {\n const signal: AbortSignal = new AbortController().signal;\n const fail = (message: string): void =>\n options.onError?.({ code: 'STORAGE_UNAVAILABLE', message });\n\n let sessionId: string | null = null;\n let available = true;\n\n const idResult: StoreResult<StoredValue | null> = await adapter.get(KEY_SESSION, { signal });\n if (idResult.ok) {\n sessionId = idResult.value?.value ?? null;\n } else {\n available = false;\n fail(`Could not read the session id: ${idResult.error}. Running in memory-only mode`);\n }\n if (available && sessionId === null) {\n sessionId = crypto.randomUUID();\n const saved: StoreResult<StoredValue> = await adapter.set(KEY_SESSION, sessionId, {\n signal,\n });\n if (!saved.ok) {\n available = false;\n fail(`Could not save the session id: ${saved.error}. Running in memory-only mode`);\n }\n }\n const id: string = sessionId ?? crypto.randomUUID();\n\n let initial: StoredValue | null = null;\n if (available) {\n const recordResult: StoreResult<StoredValue | null> = await adapter.get(keyRecord(id), {\n signal,\n });\n if (recordResult.ok) {\n initial = recordResult.value;\n } else {\n available = false;\n fail(`Could not read saved progress: ${recordResult.error}. Running in memory-only mode`);\n }\n }\n return new AdapterSessionManager(adapter, id, available, initial, options);\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n this.record.progress = { currentPageId, answers };\n this.dirtyProgress = true;\n this.schedulePersist();\n }\n\n loadProgress(): SessionProgress | null {\n return this.record.progress;\n }\n\n saveInput(questionId: string, value: InputValue): void {\n this.record.inputs[questionId] = value;\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n loadInput(questionId: string): InputValue | null {\n return this.record.inputs[questionId] ?? null;\n }\n\n clearAll(): void {\n this.record = emptyRecord();\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.pending = false;\n if (!this.available) return;\n void this.removeRecord();\n }\n\n dispose(): void {\n this.unsubscribe?.();\n this.unsubscribe = null;\n }\n\n /** Resolves when every save made so far has finished. Mostly for tests. */\n async flush(): Promise<void> {\n while (this.persisting) {\n await this.persisting;\n }\n }\n\n private report(message: string, cause?: unknown): void {\n this.options.onError?.({ code: 'STORAGE_UNAVAILABLE', message, cause });\n }\n\n private schedulePersist(): void {\n if (!this.available) return;\n this.pending = true;\n this.persisting ??= this.drain();\n }\n\n /** Saves until nothing is left to save. Never rejects. */\n private async drain(): Promise<void> {\n let conflicts = 0;\n try {\n while (this.pending) {\n this.pending = false;\n const outcome: 'saved' | 'conflict' | 'failed' = await this.persistOnce();\n conflicts = outcome === 'conflict' ? conflicts + 1 : 0;\n if (outcome === 'conflict') {\n if (conflicts >= MAX_CONFLICT_RETRIES) {\n this.report(\n `Gave up saving session ${this.sessionId} after ${conflicts} revision conflicts in a row`,\n );\n break;\n }\n this.pending = true;\n }\n }\n } finally {\n // Cleared in the same tick the loop ends, so a save requested right now starts a new drain.\n this.persisting = null;\n }\n }\n\n private async persistOnce(): Promise<'saved' | 'conflict' | 'failed'> {\n const sentInputs = new Set<string>(this.dirtyInputs);\n const sentProgress: boolean = this.dirtyProgress;\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n\n const result: StoreResult<StoredValue> = await this.adapter.set(\n keyRecord(this.sessionId),\n JSON.stringify(this.record),\n { signal: this.signal, ifRevision: this.revision },\n );\n if (result.ok) {\n this.revision = result.value.revision;\n return 'saved';\n }\n\n // Keep what was not saved so the next attempt includes it.\n for (const questionId of sentInputs) this.dirtyInputs.add(questionId);\n this.dirtyProgress = this.dirtyProgress || sentProgress;\n\n if (result.code !== 'conflict') {\n this.report(`Could not save session ${this.sessionId}: ${result.error}`);\n return 'failed';\n }\n const latest: StoreResult<StoredValue | null> = await this.adapter.get(\n keyRecord(this.sessionId),\n { signal: this.signal },\n );\n if (!latest.ok) {\n this.report(\n `Session ${this.sessionId} changed elsewhere, but the new version could not be read: ${latest.error}`,\n );\n return 'failed';\n }\n this.applyRemote(latest.value);\n return 'conflict';\n }\n\n /** Merges a version written elsewhere with this tab's unsaved changes. */\n private applyRemote(stored: StoredValue | null): void {\n const before: string = JSON.stringify(this.record.progress);\n if (!stored) {\n if (!this.dirtyProgress && this.dirtyInputs.size === 0) {\n this.record = emptyRecord();\n }\n this.revision = undefined;\n } else {\n const remote: SessionRecord | null = parseRecord(stored.value);\n if (!remote) {\n this.report(\n `Session ${this.sessionId} was changed elsewhere to a value that is not valid JSON; ignoring it`,\n );\n this.revision = stored.revision;\n return;\n }\n const inputs: Record<string, InputValue> = { ...remote.inputs };\n for (const questionId of this.dirtyInputs) {\n const mine: InputValue | undefined = this.record.inputs[questionId];\n if (mine !== undefined) inputs[questionId] = mine;\n }\n this.record = {\n progress: this.dirtyProgress ? this.record.progress : remote.progress,\n inputs,\n };\n this.revision = stored.revision;\n }\n if (JSON.stringify(this.record.progress) !== before) {\n this.options.onExternalChange?.(this.record.progress);\n }\n }\n\n private async removeRecord(): Promise<void> {\n const key: string = keyRecord(this.sessionId);\n const result: StoreResult<unknown> = this.adapter.remove\n ? await this.adapter.remove(key, { signal: this.signal })\n : await this.adapter.set(key, JSON.stringify(emptyRecord()), { signal: this.signal });\n if (result.ok) {\n this.revision = undefined;\n } else {\n this.report(`Could not clear session ${this.sessionId}: ${result.error}`);\n }\n }\n}\n"],"mappings":"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","names":["BaseConsoleErrorAdapter","jsonLogic","KEY_SESSION"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/adapters/browser-event-adapter.ts","../src/adapters/console-error-adapter.ts","../src/adapters/identity-input-adapter.ts","../src/rule-engine.ts","../src/schema-parser.ts","../src/session-manager.ts","../src/chat-controller.ts","../src/storage/adapter-session-manager.ts"],"sourcesContent":["export type {\n StorageAdapter,\n StoredValue,\n StoreErrorCode,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\n// The browser storage adapters live in the storage-adapters package; re-exported here so\n// they can be imported from core. Server-side adapters (SQLite, files) are in the adapters\n// package.\nexport type {\n LocalStorageAdapterOptions,\n SessionStorageAdapterOptions,\n StorageChangeEvent,\n StorageEventTarget,\n WebStorageAdapterOptions,\n WebStorageLike,\n WebStorageName,\n} from '@richardmcquiston01/storage-adapters/browser';\nexport {\n localStorageAdapter,\n sessionStorageAdapter,\n} from '@richardmcquiston01/storage-adapters/browser';\nexport type { IndexedDbAdapterOptions } from '@richardmcquiston01/storage-adapters/indexeddb';\nexport { indexedDbAdapter } from '@richardmcquiston01/storage-adapters/indexeddb';\nexport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nexport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nexport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nexport { ChatController } from './chat-controller.js';\nexport { RuleEngine } from './rule-engine.js';\nexport { parseSchema } from './schema-parser.js';\nexport type { SessionProgress, SessionStore, StorageBackend } from './session-manager.js';\nexport { SessionManager } from './session-manager.js';\nexport type { AdapterSessionManagerOptions } from './storage/adapter-session-manager.js';\nexport { AdapterSessionManager } from './storage/adapter-session-manager.js';\nexport * from './types.js';\n","import type { ChatEvent, OutputAdapter } from '../types.js';\n\nexport class BrowserEventAdapter implements OutputAdapter {\n emit(event: ChatEvent): void {\n if (typeof document === 'undefined') return;\n const name = event.type === 'page:submit' ? 'chat-page-submit' : 'chat-form-complete';\n document.dispatchEvent(new CustomEvent(name, { detail: event, bubbles: true }));\n }\n}\n","import {\n ConsoleErrorAdapter as BaseConsoleErrorAdapter,\n type ConsoleErrorOptions,\n} from '@richardmcquiston01/error-log-adapter';\nimport type { ChatErrorCode } from '../types.js';\n\n/** Writes errors to `console.error`, prefixed with `[chat-and-react]` unless overridden. */\nexport class ConsoleErrorAdapter extends BaseConsoleErrorAdapter<ChatErrorCode> {\n constructor(options: ConsoleErrorOptions = {}) {\n super({ prefix: '[chat-and-react]', ...options });\n }\n}\n","import type { InputAdapter } from '../types.js';\n\nexport class IdentityInputAdapter implements InputAdapter {\n transform(raw: unknown): unknown {\n return raw;\n }\n}\n","import jsonLogic from 'json-logic-js';\nimport type { AnswerMap, JsonLogicRule } from './types.js';\n\n/**\n * Evaluates JSON Logic rules against provided answers.\n * Wraps json-logic-js to provide type-safe rule evaluation.\n */\nexport class RuleEngine {\n /**\n * Evaluates a JSON Logic rule against an answer map.\n * @param rule The JSON Logic rule to evaluate\n * @param answers The answer map to evaluate the rule against\n * @returns The boolean result of the rule evaluation\n * @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails\n */\n evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean {\n try {\n // json-logic-js apply() returns any; cast to boolean\n return Boolean(\n (jsonLogic as { apply: (rule: unknown, data: unknown) => unknown }).apply(rule, answers),\n );\n } catch (cause) {\n throw { code: 'RULE_EVAL_FAILED', message: 'Rule evaluation failed', cause };\n }\n }\n}\n","import type { ChatPage, ChatSchema } from './types.js';\n\nconst VALID_INPUTS = new Set(['text', 'textarea', 'dropdown', 'radio', 'checkbox']);\nconst FORM_COMPLETED = '**FORM_COMPLETED**';\n\n/**\n * Parses and validates an unknown input as a {@link ChatSchema}.\n *\n * @param raw - The raw input to validate.\n * @returns The typed {@link ChatSchema} if valid.\n * @throws `{ code: 'SCHEMA_INVALID', message: string }` on any validation failure.\n */\nexport function parseSchema(raw: unknown): ChatSchema {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Schema must be a non-null object');\n }\n\n const obj = raw as Record<string, unknown>;\n\n if (obj['x-chat-version'] !== '1') {\n throw makeError('x-chat-version must be \"1\"');\n }\n\n if (!Array.isArray(obj['x-chat-pages'])) {\n throw makeError('x-chat-pages must be an array');\n }\n\n const pages = obj['x-chat-pages'] as unknown[];\n const rules = (\n typeof obj['x-chat-rules'] === 'object' && obj['x-chat-rules'] !== null\n ? obj['x-chat-rules']\n : {}\n ) as Record<string, unknown>;\n\n const pageIds = new Set<string>();\n const questionIds = new Set<string>();\n\n for (const page of pages) {\n validatePage(page, pageIds, questionIds, rules);\n }\n\n // Second pass: validate branching destinations now that all page ids are known\n for (const page of pages) {\n const p = page as ChatPage;\n for (const rule of p.branching) {\n if (rule.destination_id !== FORM_COMPLETED && !pageIds.has(rule.destination_id)) {\n throw makeError(\n `branching destination_id \"${rule.destination_id}\" does not reference a known page id`,\n );\n }\n if (rule.type === 'if' && !questionIds.has(rule.form_element)) {\n throw makeError(\n `branching form_element \"${rule.form_element}\" does not reference a known question id`,\n );\n }\n }\n }\n\n return raw as ChatSchema;\n}\n\n/**\n * Validates a single page object, collecting its id and question ids.\n *\n * @param raw - The raw page value.\n * @param pageIds - Set of already-seen page ids (mutated in place).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validatePage(\n raw: unknown,\n pageIds: Set<string>,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each page must be a non-null object');\n }\n const page = raw as Record<string, unknown>;\n\n if (typeof page['id'] !== 'string' || !page['id']) {\n throw makeError('Page id must be a non-empty string');\n }\n if (pageIds.has(page['id'] as string)) {\n throw makeError(`Duplicate page id \"${page['id']}\"`);\n }\n pageIds.add(page['id'] as string);\n\n if (!Array.isArray(page['questions'])) {\n throw makeError(`Page \"${page['id']}\" must have a questions array`);\n }\n\n const branching = page['branching'];\n if (!Array.isArray(branching) || (branching as unknown[]).length === 0) {\n throw makeError(`Page \"${page['id']}\" must have a non-empty branching array`);\n }\n\n const lastRule = (branching as unknown[])[branching.length - 1] as Record<string, unknown>;\n if (lastRule['type'] !== 'always') {\n throw makeError(`Page \"${page['id']}\" branching must end with a rule of type \"always\"`);\n }\n\n for (const q of page['questions'] as unknown[]) {\n validateQuestion(q, page['id'] as string, questionIds, rules);\n }\n}\n\n/**\n * Validates a single question object, collecting its id.\n *\n * @param raw - The raw question value.\n * @param pageId - The id of the containing page (for error messages).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validateQuestion(\n raw: unknown,\n pageId: string,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each question must be a non-null object');\n }\n const q = raw as Record<string, unknown>;\n\n if (typeof q['id'] !== 'string' || !q['id']) {\n throw makeError(`In page \"${pageId}\": question id must be a non-empty string`);\n }\n if (questionIds.has(q['id'] as string)) {\n throw makeError(`Duplicate question id \"${q['id']}\"`);\n }\n questionIds.add(q['id'] as string);\n\n if (!VALID_INPUTS.has(q['x-chat-input'] as string)) {\n throw makeError(\n `Question \"${q['id']}\" has invalid x-chat-input \"${q['x-chat-input']}\". ` +\n `Must be one of: text, textarea, dropdown, radio, checkbox`,\n );\n }\n\n const condition = q['x-chat-condition'];\n if (condition && typeof condition === 'object' && !Array.isArray(condition)) {\n const c = condition as Record<string, unknown>;\n if ('ref' in c && typeof c['ref'] === 'string') {\n if (!(c['ref'] in rules)) {\n throw makeError(`Question \"${q['id']}\" x-chat-condition refs unknown rule \"${c['ref']}\"`);\n }\n }\n }\n}\n\n/**\n * Creates a schema validation error object.\n *\n * @param message - Human-readable description of the validation failure.\n * @returns An error object with `code: 'SCHEMA_INVALID'`.\n */\nfunction makeError(message: string): { code: 'SCHEMA_INVALID'; message: string } {\n return { code: 'SCHEMA_INVALID', message };\n}\n","// packages/core/src/session-manager.ts\nimport type { AnswerMap } from './types.js';\n\nexport interface StorageBackend {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n key(index: number): string | null;\n readonly length: number;\n}\n\nexport interface SessionProgress {\n currentPageId: string;\n answers: AnswerMap;\n}\n\nconst KEY_SESSION = 'car:session-id';\nconst keyProgress = (id: string) => `car:progress:${id}`;\nconst keyInput = (sessionId: string, questionId: string) => `car:input:${sessionId}:${questionId}`;\n\n/**\n * What `ChatController` needs from session storage. `SessionManager` keeps it in a\n * synchronous Web Storage object; `AdapterSessionManager` keeps it in a `StorageAdapter`.\n */\nexport interface SessionStore {\n readonly isStorageAvailable: boolean;\n getSessionId(): string;\n saveProgress(currentPageId: string, answers: AnswerMap): void;\n loadProgress(): SessionProgress | null;\n saveInput(questionId: string, value: string | string[]): void;\n loadInput(questionId: string): string | string[] | null;\n /** Forgets a saved input, such as a draft that has been submitted. */\n clearInput(questionId: string): void;\n clearAll(): void;\n /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */\n dispose?(): void;\n}\n\nexport class SessionManager implements SessionStore {\n private readonly sessionId: string;\n private readonly available: boolean;\n\n constructor(private readonly storage: StorageBackend = globalThis.localStorage) {\n this.available = this.checkStorage();\n this.sessionId = this.loadOrCreateSessionId();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n if (!this.available) return;\n this.storage.setItem(keyProgress(this.sessionId), JSON.stringify({ currentPageId, answers }));\n }\n\n loadProgress(): SessionProgress | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyProgress(this.sessionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as SessionProgress;\n } catch {\n return null;\n }\n }\n\n saveInput(questionId: string, value: string | string[]): void {\n if (!this.available) return;\n this.storage.setItem(keyInput(this.sessionId, questionId), JSON.stringify(value));\n }\n\n loadInput(questionId: string): string | string[] | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyInput(this.sessionId, questionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as string | string[];\n } catch {\n return null;\n }\n }\n\n clearInput(questionId: string): void {\n if (!this.available) return;\n this.storage.removeItem(keyInput(this.sessionId, questionId));\n }\n\n clearAll(): void {\n if (!this.available) return;\n const progressKey = keyProgress(this.sessionId);\n const inputPrefix = `car:input:${this.sessionId}:`;\n const keysToRemove: string[] = [progressKey];\n\n for (let i = 0; i < this.storage.length; i++) {\n const k = this.storage.key(i);\n if (k?.startsWith(inputPrefix)) keysToRemove.push(k);\n }\n\n for (const k of keysToRemove) {\n this.storage.removeItem(k);\n }\n }\n\n private loadOrCreateSessionId(): string {\n if (!this.available) return crypto.randomUUID();\n const existing = this.storage.getItem(KEY_SESSION);\n if (existing) return existing;\n const id = crypto.randomUUID();\n this.storage.setItem(KEY_SESSION, id);\n return id;\n }\n\n private checkStorage(): boolean {\n try {\n const probe = '__car_probe__';\n this.storage.setItem(probe, '1');\n this.storage.removeItem(probe);\n return true;\n } catch {\n return false;\n }\n }\n}\n","// packages/core/src/chat-controller.ts\n\nimport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nimport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nimport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nimport { RuleEngine } from './rule-engine.js';\nimport { parseSchema } from './schema-parser.js';\nimport { SessionManager, type SessionStore } from './session-manager.js';\nimport type {\n AnswerMap,\n ChatControllerConfig,\n ChatError,\n ChatEvent,\n ChatOptions,\n ChatPage,\n ChatQuestion,\n ChatSchema,\n ErrorLogResult,\n RouteEntry,\n SubmitOutcome,\n} from './types.js';\n\ntype EventName = 'page:submit' | 'form:complete';\ntype Listener = (event: ChatEvent) => void;\n\nconst DEFAULT_OPTIONS: ChatOptions = {\n userResumable: true,\n localStorage: true,\n autoComplete: true,\n allowEdit: false,\n autoHideQuestions: 0,\n autoHideAnswers: 0,\n hideAnimation: 'fade',\n typingSpeed: 0,\n thinkingDelay: 0,\n thinkingText: '',\n showContinueIcon: true,\n};\n\n/** Copies an object without its undefined values, so they cannot hide a default. */\nfunction stripUndefined<T extends object>(source: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(source).filter(([, value]) => value !== undefined),\n ) as Partial<T>;\n}\n\n/**\n * Central orchestrator for the chat-and-react form flow.\n * Drives page navigation, branching, session persistence, and event emission.\n */\nexport class ChatController {\n private schema: ChatSchema | null = null;\n private options: ChatOptions = { ...DEFAULT_OPTIONS };\n private readonly config: Required<ChatControllerConfig>;\n private readonly session: SessionStore;\n private readonly ruleEngine = new RuleEngine();\n private currentPage: ChatPage | null = null;\n private allAnswers: AnswerMap = {};\n private visitIndex = 0;\n /** Pages submitted in this session, in visit order. Used for editing earlier answers. */\n private route: RouteEntry[] = [];\n /** Index in `route` of the page being edited, or null when not editing. */\n private editIndex: number | null = null;\n /** The page the user was on before they started editing an earlier one. */\n private resumePageId: string | null = null;\n private readonly listeners = new Map<EventName, Set<Listener>>();\n\n /**\n * @param config - Controller configuration including schema and optional adapters.\n * @param sessionManager - Optional session manager for test injection.\n */\n constructor(config: ChatControllerConfig, sessionManager?: SessionStore) {\n this.config = {\n inputAdapter: config.inputAdapter ?? new IdentityInputAdapter(),\n outputAdapter: config.outputAdapter ?? new BrowserEventAdapter(),\n errorAdapter: config.errorAdapter ?? new ConsoleErrorAdapter(),\n schema: config.schema,\n options: config.options ?? {},\n };\n this.session = sessionManager ?? new SessionManager();\n }\n\n /**\n * Initialises the controller: parses the schema, resolves options,\n * restores any saved session, and positions to the first page.\n */\n start(): void {\n let raw: unknown;\n try {\n raw = this.config.inputAdapter.transform(this.config.schema);\n } catch (cause) {\n this.handleFatal({ code: 'ADAPTER_ERROR', message: 'InputAdapter.transform() threw', cause });\n }\n\n let schema: ChatSchema;\n try {\n schema = parseSchema(raw);\n } catch (e) {\n this.handleFatal(e as ChatError);\n }\n this.schema = schema;\n\n if (!this.session.isStorageAvailable) {\n this.logError({\n code: 'STORAGE_UNAVAILABLE',\n message: 'localStorage unavailable; running in memory-only mode',\n });\n }\n\n // Defaults, then the schema, then overrides passed in by the host (for\n // example element attributes).\n const schemaOpts = schema['x-chat-options'] ?? {};\n this.options = {\n ...DEFAULT_OPTIONS,\n ...stripUndefined(schemaOpts),\n ...stripUndefined(this.config.options),\n };\n\n if (this.options.userResumable) {\n const saved = this.session.loadProgress();\n if (saved) {\n const page = schema['x-chat-pages'].find((p) => p.id === saved.currentPageId);\n if (page) {\n this.allAnswers = saved.answers;\n this.currentPage = page;\n return;\n }\n }\n }\n\n this.currentPage = schema['x-chat-pages'][0];\n }\n\n /**\n * Returns the page currently being presented to the user, or null if complete.\n */\n getCurrentPage(): ChatPage | null {\n return this.currentPage;\n }\n\n /**\n * Returns the subset of questions on the current page that pass their\n * `x-chat-condition` rule (if any). Questions without a condition are always visible.\n */\n getVisibleQuestions(): ChatQuestion[] {\n if (!this.currentPage) return [];\n return this.currentPage.questions.filter((q) => this.isQuestionVisible(q));\n }\n\n /**\n * Returns the resolved options for the current session (schema options merged with defaults).\n */\n getOptions(): ChatOptions {\n return { ...this.options };\n }\n\n /**\n * Returns the pages the user has submitted in this session, in visit order.\n * A session restored from storage starts with an empty route.\n */\n getRoute(): RouteEntry[] {\n return this.route.map((entry) => ({ pageId: entry.pageId, answers: { ...entry.answers } }));\n }\n\n /**\n * Returns every answer given so far, for pre-filling inputs.\n */\n getAnswers(): AnswerMap {\n return { ...this.allAnswers };\n }\n\n /**\n * Re-opens a page the user already submitted so its answers can be changed.\n * Call `submitPage` afterwards to save the new answers. Not available once the\n * form is complete.\n *\n * @param routeIndex - Position of the page in `getRoute()`.\n * @returns true when the page was re-opened.\n */\n reopenPage(routeIndex: number): boolean {\n if (!this.schema || !this.currentPage) return false;\n const entry = this.route[routeIndex];\n if (!entry) return false;\n const page = this.schema['x-chat-pages'].find((p) => p.id === entry.pageId);\n if (!page) return false;\n\n // Remember where the user was, but only the first time: editing another\n // earlier page before finishing this edit must still return to that page.\n if (this.editIndex === null) this.resumePageId = this.currentPage.id;\n this.editIndex = routeIndex;\n this.currentPage = page;\n return true;\n }\n\n /**\n * Submits the answers for the current page and advances the session.\n * - Accumulates answers into `allAnswers`.\n * - Emits `page:submit` (suppressed when `userResumable` is false).\n * - Resolves branching to find the next page.\n * - Emits `form:complete` and clears session when `**FORM_COMPLETED**` is reached.\n *\n * When the page was re-opened with `reopenPage`, the new answers replace the\n * old ones and `page:submit` is emitted with `edited: true`. If the page's\n * branching still leads to the same next page, the form returns to the page the\n * user was on. If it leads somewhere else, the pages that followed it are\n * dropped, along with their answers, and the form continues from the new page.\n *\n * @param pageAnswers - Answers collected for the current page.\n */\n submitPage(pageAnswers: AnswerMap): SubmitOutcome {\n if (!this.currentPage || !this.schema) return 'advanced';\n\n const pageId = this.currentPage.id;\n const currentVisitIndex = this.visitIndex;\n const editedIndex = this.editIndex;\n const isEdit = editedIndex !== null;\n\n let outcome: SubmitOutcome = 'advanced';\n let nextPageId: string;\n let resumePageId: string | null = null;\n\n if (isEdit) {\n const entry = this.route[editedIndex];\n // Replace the old answers for this page, including any question that is\n // no longer visible after the change.\n const remaining = { ...this.allAnswers };\n for (const questionId of Object.keys(entry.answers)) delete remaining[questionId];\n this.allAnswers = { ...remaining, ...pageAnswers };\n entry.answers = { ...pageAnswers };\n\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n const expectedNextPageId = this.route[editedIndex + 1]?.pageId ?? this.resumePageId;\n\n if (nextPageId === expectedNextPageId) {\n outcome = 'edit-resumed';\n resumePageId = this.resumePageId;\n } else {\n outcome = 'edit-rerouted';\n // The route changed: forget the pages that followed and their answers.\n for (const later of this.route.slice(editedIndex + 1)) {\n for (const questionId of Object.keys(later.answers)) delete this.allAnswers[questionId];\n }\n this.route.length = editedIndex + 1;\n }\n this.editIndex = null;\n this.resumePageId = null;\n } else {\n this.allAnswers = { ...this.allAnswers, ...pageAnswers };\n this.route.push({ pageId, answers: { ...pageAnswers } });\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n }\n\n if (this.options.userResumable) {\n const progressPageId =\n outcome === 'edit-resumed' && resumePageId\n ? resumePageId\n : nextPageId === '**FORM_COMPLETED**'\n ? pageId\n : nextPageId;\n this.session.saveProgress(progressPageId, this.allAnswers);\n\n const submitEvent: ChatEvent = {\n type: 'page:submit',\n sessionId: this.session.getSessionId(),\n pageId,\n visitIndex: currentVisitIndex,\n answers: pageAnswers,\n ...(isEdit ? { edited: true } : {}),\n };\n this.safeEmit(submitEvent);\n this.emit('page:submit', submitEvent);\n }\n\n this.visitIndex++;\n\n if (outcome === 'edit-resumed' && resumePageId) {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === resumePageId) ?? null;\n return outcome;\n }\n\n if (nextPageId === '**FORM_COMPLETED**') {\n this.session.clearAll();\n const completeEvent: ChatEvent = {\n type: 'form:complete',\n sessionId: this.session.getSessionId(),\n allAnswers: this.allAnswers,\n };\n this.safeEmit(completeEvent);\n this.emit('form:complete', completeEvent);\n this.currentPage = null;\n } else {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === nextPageId) ?? null;\n }\n return outcome;\n }\n\n /**\n * Registers an event listener for the given event type.\n *\n * @param event - The event to listen for.\n * @param listener - The callback to invoke when the event fires.\n */\n on(event: EventName, listener: Listener): void {\n let listenersForEvent = this.listeners.get(event);\n if (!listenersForEvent) {\n listenersForEvent = new Set();\n this.listeners.set(event, listenersForEvent);\n }\n listenersForEvent.add(listener);\n }\n\n /**\n * Removes a previously registered event listener.\n *\n * @param event - The event type to deregister from.\n * @param listener - The specific listener to remove.\n */\n off(event: EventName, listener: Listener): void {\n this.listeners.get(event)?.delete(listener);\n }\n\n /**\n * Tears down the controller: removes all listeners and nulls internal state.\n */\n destroy(): void {\n this.session.dispose?.();\n this.listeners.clear();\n this.currentPage = null;\n this.schema = null;\n }\n\n /**\n * Evaluates whether a question should be shown based on its `x-chat-condition`.\n * Returns true when no condition is present. On evaluation failure, logs and returns false.\n */\n private isQuestionVisible(question: ChatQuestion): boolean {\n const condition = question['x-chat-condition'];\n if (!condition) return true;\n\n const rules = this.schema?.['x-chat-rules'] ?? {};\n let rule: Record<string, unknown>;\n\n if ('ref' in condition && typeof (condition as { ref?: unknown }).ref === 'string') {\n const ref = (condition as { ref: string }).ref;\n rule = rules[ref];\n if (!rule) return false;\n } else {\n rule = condition as Record<string, unknown>;\n }\n\n try {\n return this.ruleEngine.evaluate(rule, this.answersForRules());\n } catch (e) {\n this.logError(e as ChatError);\n return false;\n }\n }\n\n /**\n * The answers question conditions are checked against. While an earlier page is\n * being edited, that page's own answers are left out, so its questions are\n * shown exactly as they were the first time the page was asked.\n */\n private answersForRules(): AnswerMap {\n if (this.editIndex === null) return this.allAnswers;\n const rest = { ...this.allAnswers };\n for (const questionId of Object.keys(this.route[this.editIndex].answers))\n delete rest[questionId];\n return rest;\n }\n\n /**\n * Walks the branching rules top-to-bottom. Returns the first match's destination_id.\n * Falls back to `**FORM_COMPLETED**` if nothing matches (should not happen with valid schemas).\n */\n private resolveBranching(page: ChatPage, answers: AnswerMap): string {\n for (const rule of page.branching) {\n if (rule.type === 'always') return rule.destination_id;\n if (rule.type === 'if' && answers[rule.form_element] === rule.value) {\n return rule.destination_id;\n }\n }\n return '**FORM_COMPLETED**';\n }\n\n /**\n * Calls `outputAdapter.emit()` and swallows adapter errors (logging them instead).\n */\n private safeEmit(event: ChatEvent): void {\n try {\n this.config.outputAdapter.emit(event);\n } catch (cause) {\n this.logError({\n code: 'ADAPTER_ERROR',\n message: `OutputAdapter.emit() threw for ${event.type}`,\n cause,\n });\n }\n }\n\n /**\n * Notifies all registered listeners for the given event.\n */\n private emit(event: EventName, payload: ChatEvent): void {\n this.listeners.get(event)?.forEach((l) => {\n l(payload);\n });\n }\n\n /**\n * Logs the error via the error adapter and then throws it.\n * Used for unrecoverable errors (schema invalid, adapter failure on start).\n */\n private handleFatal(error: ChatError): never {\n this.logError(error);\n throw error;\n }\n\n /**\n * Sends the error to the error adapter without waiting for it. If the adapter\n * reports a failure, rejects, or throws, that is written to `console.error` so\n * the original error is never lost and the form keeps running.\n */\n private logError(entry: ChatError): void {\n const reportFailure = (reason: unknown): void => {\n console.error('[chat-and-react] ErrorLogAdapter failed to record an error:', reason, entry);\n };\n try {\n // `Promise.resolve` also tolerates older custom adapters that return nothing.\n void Promise.resolve(this.config.errorAdapter.log(entry)).then(\n (result: ErrorLogResult | undefined): void => {\n if (result?.ok === false) {\n reportFailure(result.error);\n }\n },\n reportFailure,\n );\n } catch (cause) {\n reportFailure(cause);\n }\n }\n}\n","import type {\n StorageAdapter,\n StoredValue,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\nimport type { SessionProgress, SessionStore } from '../session-manager.js';\nimport type { AnswerMap, ChatError } from '../types.js';\n\nconst KEY_SESSION = 'car:session-id';\nconst keyRecord = (sessionId: string): string => `car:session:${sessionId}`;\n\n/** After this many revision conflicts in a row, a save is abandoned and reported. */\nconst MAX_CONFLICT_RETRIES = 3;\n\ntype InputValue = string | string[];\n\n/** Everything saved for one session, stored under a single key. */\ninterface SessionRecord {\n progress: SessionProgress | null;\n inputs: Record<string, InputValue>;\n}\n\nexport interface AdapterSessionManagerOptions {\n /** Called when the adapter fails. The session keeps working from memory. */\n onError?: (error: ChatError) => void;\n /**\n * Called when saved progress changes because of a write made elsewhere, such as\n * another tab. `null` means the session was cleared there.\n */\n onExternalChange?: (progress: SessionProgress | null) => void;\n}\n\nconst emptyRecord = (): SessionRecord => ({ progress: null, inputs: {} });\n\nfunction parseRecord(raw: string): SessionRecord | null {\n try {\n const parsed = JSON.parse(raw) as Partial<SessionRecord>;\n return {\n progress: parsed.progress ?? null,\n inputs: parsed.inputs && typeof parsed.inputs === 'object' ? parsed.inputs : {},\n };\n } catch {\n return null;\n }\n}\n\n/**\n * A session store backed by a `StorageAdapter`. `ChatController` is synchronous, so\n * the saved session is loaded into memory by `create()`, reads come from memory, and\n * writes are saved in the background. Failures are reported through `onError`.\n *\n * Saves carry the revision they were based on. If another tab wrote first, the two\n * are merged: inputs per question, and progress only when this tab saved progress\n * itself, so a tab that only typed cannot overwrite another tab's progress.\n */\nexport class AdapterSessionManager implements SessionStore {\n private record: SessionRecord;\n private revision: string | undefined;\n private readonly dirtyInputs = new Set<string>();\n private dirtyProgress = false;\n private pending = false;\n private persisting: Promise<void> | null = null;\n private readonly signal: AbortSignal = new AbortController().signal;\n private unsubscribe: (() => void) | null = null;\n\n private constructor(\n private readonly adapter: StorageAdapter,\n private readonly sessionId: string,\n private readonly available: boolean,\n initial: StoredValue | null,\n private readonly options: AdapterSessionManagerOptions,\n ) {\n const parsed: SessionRecord | null = initial ? parseRecord(initial.value) : null;\n this.record = parsed ?? emptyRecord();\n this.revision = initial?.revision;\n if (initial && !parsed) {\n this.report(\n `Saved session ${sessionId} could not be read as JSON; starting with an empty session`,\n );\n }\n if (available) {\n this.unsubscribe =\n adapter.subscribe?.(keyRecord(sessionId), (stored) => this.applyRemote(stored)) ?? null;\n }\n }\n\n /** Loads (or creates) the session id and its saved record, then returns the manager. */\n static async create(\n adapter: StorageAdapter,\n options: AdapterSessionManagerOptions = {},\n ): Promise<AdapterSessionManager> {\n const signal: AbortSignal = new AbortController().signal;\n const fail = (message: string): void =>\n options.onError?.({ code: 'STORAGE_UNAVAILABLE', message });\n\n let sessionId: string | null = null;\n let available = true;\n\n const idResult: StoreResult<StoredValue | null> = await adapter.get(KEY_SESSION, { signal });\n if (idResult.ok) {\n sessionId = idResult.value?.value ?? null;\n } else {\n available = false;\n fail(`Could not read the session id: ${idResult.error}. Running in memory-only mode`);\n }\n if (available && sessionId === null) {\n sessionId = crypto.randomUUID();\n const saved: StoreResult<StoredValue> = await adapter.set(KEY_SESSION, sessionId, {\n signal,\n });\n if (!saved.ok) {\n available = false;\n fail(`Could not save the session id: ${saved.error}. Running in memory-only mode`);\n }\n }\n const id: string = sessionId ?? crypto.randomUUID();\n\n let initial: StoredValue | null = null;\n if (available) {\n const recordResult: StoreResult<StoredValue | null> = await adapter.get(keyRecord(id), {\n signal,\n });\n if (recordResult.ok) {\n initial = recordResult.value;\n } else {\n available = false;\n fail(`Could not read saved progress: ${recordResult.error}. Running in memory-only mode`);\n }\n }\n return new AdapterSessionManager(adapter, id, available, initial, options);\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n this.record.progress = { currentPageId, answers };\n this.dirtyProgress = true;\n this.schedulePersist();\n }\n\n loadProgress(): SessionProgress | null {\n return this.record.progress;\n }\n\n saveInput(questionId: string, value: InputValue): void {\n this.record.inputs[questionId] = value;\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n loadInput(questionId: string): InputValue | null {\n return this.record.inputs[questionId] ?? null;\n }\n\n clearInput(questionId: string): void {\n if (!(questionId in this.record.inputs)) return;\n delete this.record.inputs[questionId];\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n clearAll(): void {\n this.record = emptyRecord();\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.pending = false;\n if (!this.available) return;\n void this.removeRecord();\n }\n\n dispose(): void {\n this.unsubscribe?.();\n this.unsubscribe = null;\n }\n\n /** Resolves when every save made so far has finished. Mostly for tests. */\n async flush(): Promise<void> {\n while (this.persisting) {\n await this.persisting;\n }\n }\n\n private report(message: string, cause?: unknown): void {\n this.options.onError?.({ code: 'STORAGE_UNAVAILABLE', message, cause });\n }\n\n private schedulePersist(): void {\n if (!this.available) return;\n this.pending = true;\n this.persisting ??= this.drain();\n }\n\n /** Saves until nothing is left to save. Never rejects. */\n private async drain(): Promise<void> {\n let conflicts = 0;\n try {\n while (this.pending) {\n this.pending = false;\n const outcome: 'saved' | 'conflict' | 'failed' = await this.persistOnce();\n conflicts = outcome === 'conflict' ? conflicts + 1 : 0;\n if (outcome === 'conflict') {\n if (conflicts >= MAX_CONFLICT_RETRIES) {\n this.report(\n `Gave up saving session ${this.sessionId} after ${conflicts} revision conflicts in a row`,\n );\n break;\n }\n this.pending = true;\n }\n }\n } finally {\n // Cleared in the same tick the loop ends, so a save requested right now starts a new drain.\n this.persisting = null;\n }\n }\n\n private async persistOnce(): Promise<'saved' | 'conflict' | 'failed'> {\n const sentInputs = new Set<string>(this.dirtyInputs);\n const sentProgress: boolean = this.dirtyProgress;\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n\n const result: StoreResult<StoredValue> = await this.adapter.set(\n keyRecord(this.sessionId),\n JSON.stringify(this.record),\n { signal: this.signal, ifRevision: this.revision },\n );\n if (result.ok) {\n this.revision = result.value.revision;\n return 'saved';\n }\n\n // Keep what was not saved so the next attempt includes it.\n for (const questionId of sentInputs) this.dirtyInputs.add(questionId);\n this.dirtyProgress = this.dirtyProgress || sentProgress;\n\n if (result.code !== 'conflict') {\n this.report(`Could not save session ${this.sessionId}: ${result.error}`);\n return 'failed';\n }\n const latest: StoreResult<StoredValue | null> = await this.adapter.get(\n keyRecord(this.sessionId),\n { signal: this.signal },\n );\n if (!latest.ok) {\n this.report(\n `Session ${this.sessionId} changed elsewhere, but the new version could not be read: ${latest.error}`,\n );\n return 'failed';\n }\n this.applyRemote(latest.value);\n return 'conflict';\n }\n\n /** Merges a version written elsewhere with this tab's unsaved changes. */\n private applyRemote(stored: StoredValue | null): void {\n const before: string = JSON.stringify(this.record.progress);\n if (!stored) {\n if (!this.dirtyProgress && this.dirtyInputs.size === 0) {\n this.record = emptyRecord();\n }\n this.revision = undefined;\n } else {\n const remote: SessionRecord | null = parseRecord(stored.value);\n if (!remote) {\n this.report(\n `Session ${this.sessionId} was changed elsewhere to a value that is not valid JSON; ignoring it`,\n );\n this.revision = stored.revision;\n return;\n }\n const inputs: Record<string, InputValue> = { ...remote.inputs };\n for (const questionId of this.dirtyInputs) {\n const mine: InputValue | undefined = this.record.inputs[questionId];\n // An input that is dirty but missing here was cleared on this tab.\n if (mine !== undefined) {\n inputs[questionId] = mine;\n } else {\n delete inputs[questionId];\n }\n }\n this.record = {\n progress: this.dirtyProgress ? this.record.progress : remote.progress,\n inputs,\n };\n this.revision = stored.revision;\n }\n if (JSON.stringify(this.record.progress) !== before) {\n this.options.onExternalChange?.(this.record.progress);\n }\n }\n\n private async removeRecord(): Promise<void> {\n const key: string = keyRecord(this.sessionId);\n const result: StoreResult<unknown> = this.adapter.remove\n ? await this.adapter.remove(key, { signal: this.signal })\n : await this.adapter.set(key, JSON.stringify(emptyRecord()), { signal: this.signal });\n if (result.ok) {\n this.revision = undefined;\n } else {\n this.report(`Could not clear session ${this.sessionId}: ${result.error}`);\n }\n }\n}\n"],"mappings":"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","names":["BaseConsoleErrorAdapter","jsonLogic","KEY_SESSION"]}
package/dist/index.d.cts CHANGED
@@ -170,6 +170,8 @@ interface SessionStore {
170
170
  loadProgress(): SessionProgress | null;
171
171
  saveInput(questionId: string, value: string | string[]): void;
172
172
  loadInput(questionId: string): string | string[] | null;
173
+ /** Forgets a saved input, such as a draft that has been submitted. */
174
+ clearInput(questionId: string): void;
173
175
  clearAll(): void;
174
176
  /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */
175
177
  dispose?(): void;
@@ -185,6 +187,7 @@ declare class SessionManager implements SessionStore {
185
187
  loadProgress(): SessionProgress | null;
186
188
  saveInput(questionId: string, value: string | string[]): void;
187
189
  loadInput(questionId: string): string | string[] | null;
190
+ clearInput(questionId: string): void;
188
191
  clearAll(): void;
189
192
  private loadOrCreateSessionId;
190
193
  private checkStorage;
@@ -389,6 +392,7 @@ declare class AdapterSessionManager implements SessionStore {
389
392
  loadProgress(): SessionProgress | null;
390
393
  saveInput(questionId: string, value: InputValue): void;
391
394
  loadInput(questionId: string): InputValue | null;
395
+ clearInput(questionId: string): void;
392
396
  clearAll(): void;
393
397
  dispose(): void;
394
398
  /** Resolves when every save made so far has finished. Mostly for tests. */
package/dist/index.d.ts CHANGED
@@ -170,6 +170,8 @@ interface SessionStore {
170
170
  loadProgress(): SessionProgress | null;
171
171
  saveInput(questionId: string, value: string | string[]): void;
172
172
  loadInput(questionId: string): string | string[] | null;
173
+ /** Forgets a saved input, such as a draft that has been submitted. */
174
+ clearInput(questionId: string): void;
173
175
  clearAll(): void;
174
176
  /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */
175
177
  dispose?(): void;
@@ -185,6 +187,7 @@ declare class SessionManager implements SessionStore {
185
187
  loadProgress(): SessionProgress | null;
186
188
  saveInput(questionId: string, value: string | string[]): void;
187
189
  loadInput(questionId: string): string | string[] | null;
190
+ clearInput(questionId: string): void;
188
191
  clearAll(): void;
189
192
  private loadOrCreateSessionId;
190
193
  private checkStorage;
@@ -389,6 +392,7 @@ declare class AdapterSessionManager implements SessionStore {
389
392
  loadProgress(): SessionProgress | null;
390
393
  saveInput(questionId: string, value: InputValue): void;
391
394
  loadInput(questionId: string): InputValue | null;
395
+ clearInput(questionId: string): void;
392
396
  clearAll(): void;
393
397
  dispose(): void;
394
398
  /** Resolves when every save made so far has finished. Mostly for tests. */
package/dist/index.mjs CHANGED
@@ -192,6 +192,10 @@ var SessionManager = class {
192
192
  return null;
193
193
  }
194
194
  }
195
+ clearInput(questionId) {
196
+ if (!this.available) return;
197
+ this.storage.removeItem(keyInput(this.sessionId, questionId));
198
+ }
195
199
  clearAll() {
196
200
  if (!this.available) return;
197
201
  const progressKey = keyProgress(this.sessionId);
@@ -684,6 +688,12 @@ var AdapterSessionManager = class _AdapterSessionManager {
684
688
  loadInput(questionId) {
685
689
  return this.record.inputs[questionId] ?? null;
686
690
  }
691
+ clearInput(questionId) {
692
+ if (!(questionId in this.record.inputs)) return;
693
+ delete this.record.inputs[questionId];
694
+ this.dirtyInputs.add(questionId);
695
+ this.schedulePersist();
696
+ }
687
697
  clearAll() {
688
698
  this.record = emptyRecord();
689
699
  this.dirtyInputs.clear();
@@ -785,7 +795,11 @@ var AdapterSessionManager = class _AdapterSessionManager {
785
795
  const inputs = { ...remote.inputs };
786
796
  for (const questionId of this.dirtyInputs) {
787
797
  const mine = this.record.inputs[questionId];
788
- if (mine !== void 0) inputs[questionId] = mine;
798
+ if (mine !== void 0) {
799
+ inputs[questionId] = mine;
800
+ } else {
801
+ delete inputs[questionId];
802
+ }
789
803
  }
790
804
  this.record = {
791
805
  progress: this.dirtyProgress ? this.record.progress : remote.progress,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/adapters/browser-event-adapter.ts","../src/adapters/console-error-adapter.ts","../src/adapters/identity-input-adapter.ts","../src/rule-engine.ts","../src/schema-parser.ts","../src/session-manager.ts","../src/chat-controller.ts","../src/storage/adapter-session-manager.ts"],"sourcesContent":["export type {\n StorageAdapter,\n StoredValue,\n StoreErrorCode,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\n// The browser storage adapters live in the storage-adapters package; re-exported here so\n// they can be imported from core. Server-side adapters (SQLite, files) are in the adapters\n// package.\nexport type {\n LocalStorageAdapterOptions,\n SessionStorageAdapterOptions,\n StorageChangeEvent,\n StorageEventTarget,\n WebStorageAdapterOptions,\n WebStorageLike,\n WebStorageName,\n} from '@richardmcquiston01/storage-adapters/browser';\nexport {\n localStorageAdapter,\n sessionStorageAdapter,\n} from '@richardmcquiston01/storage-adapters/browser';\nexport type { IndexedDbAdapterOptions } from '@richardmcquiston01/storage-adapters/indexeddb';\nexport { indexedDbAdapter } from '@richardmcquiston01/storage-adapters/indexeddb';\nexport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nexport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nexport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nexport { ChatController } from './chat-controller.js';\nexport { RuleEngine } from './rule-engine.js';\nexport { parseSchema } from './schema-parser.js';\nexport type { SessionProgress, SessionStore, StorageBackend } from './session-manager.js';\nexport { SessionManager } from './session-manager.js';\nexport type { AdapterSessionManagerOptions } from './storage/adapter-session-manager.js';\nexport { AdapterSessionManager } from './storage/adapter-session-manager.js';\nexport * from './types.js';\n","import type { ChatEvent, OutputAdapter } from '../types.js';\n\nexport class BrowserEventAdapter implements OutputAdapter {\n emit(event: ChatEvent): void {\n if (typeof document === 'undefined') return;\n const name = event.type === 'page:submit' ? 'chat-page-submit' : 'chat-form-complete';\n document.dispatchEvent(new CustomEvent(name, { detail: event, bubbles: true }));\n }\n}\n","import {\n ConsoleErrorAdapter as BaseConsoleErrorAdapter,\n type ConsoleErrorOptions,\n} from '@richardmcquiston01/error-log-adapter';\nimport type { ChatErrorCode } from '../types.js';\n\n/** Writes errors to `console.error`, prefixed with `[chat-and-react]` unless overridden. */\nexport class ConsoleErrorAdapter extends BaseConsoleErrorAdapter<ChatErrorCode> {\n constructor(options: ConsoleErrorOptions = {}) {\n super({ prefix: '[chat-and-react]', ...options });\n }\n}\n","import type { InputAdapter } from '../types.js';\n\nexport class IdentityInputAdapter implements InputAdapter {\n transform(raw: unknown): unknown {\n return raw;\n }\n}\n","import jsonLogic from 'json-logic-js';\nimport type { AnswerMap, JsonLogicRule } from './types.js';\n\n/**\n * Evaluates JSON Logic rules against provided answers.\n * Wraps json-logic-js to provide type-safe rule evaluation.\n */\nexport class RuleEngine {\n /**\n * Evaluates a JSON Logic rule against an answer map.\n * @param rule The JSON Logic rule to evaluate\n * @param answers The answer map to evaluate the rule against\n * @returns The boolean result of the rule evaluation\n * @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails\n */\n evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean {\n try {\n // json-logic-js apply() returns any; cast to boolean\n return Boolean(\n (jsonLogic as { apply: (rule: unknown, data: unknown) => unknown }).apply(rule, answers),\n );\n } catch (cause) {\n throw { code: 'RULE_EVAL_FAILED', message: 'Rule evaluation failed', cause };\n }\n }\n}\n","import type { ChatPage, ChatSchema } from './types.js';\n\nconst VALID_INPUTS = new Set(['text', 'textarea', 'dropdown', 'radio', 'checkbox']);\nconst FORM_COMPLETED = '**FORM_COMPLETED**';\n\n/**\n * Parses and validates an unknown input as a {@link ChatSchema}.\n *\n * @param raw - The raw input to validate.\n * @returns The typed {@link ChatSchema} if valid.\n * @throws `{ code: 'SCHEMA_INVALID', message: string }` on any validation failure.\n */\nexport function parseSchema(raw: unknown): ChatSchema {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Schema must be a non-null object');\n }\n\n const obj = raw as Record<string, unknown>;\n\n if (obj['x-chat-version'] !== '1') {\n throw makeError('x-chat-version must be \"1\"');\n }\n\n if (!Array.isArray(obj['x-chat-pages'])) {\n throw makeError('x-chat-pages must be an array');\n }\n\n const pages = obj['x-chat-pages'] as unknown[];\n const rules = (\n typeof obj['x-chat-rules'] === 'object' && obj['x-chat-rules'] !== null\n ? obj['x-chat-rules']\n : {}\n ) as Record<string, unknown>;\n\n const pageIds = new Set<string>();\n const questionIds = new Set<string>();\n\n for (const page of pages) {\n validatePage(page, pageIds, questionIds, rules);\n }\n\n // Second pass: validate branching destinations now that all page ids are known\n for (const page of pages) {\n const p = page as ChatPage;\n for (const rule of p.branching) {\n if (rule.destination_id !== FORM_COMPLETED && !pageIds.has(rule.destination_id)) {\n throw makeError(\n `branching destination_id \"${rule.destination_id}\" does not reference a known page id`,\n );\n }\n if (rule.type === 'if' && !questionIds.has(rule.form_element)) {\n throw makeError(\n `branching form_element \"${rule.form_element}\" does not reference a known question id`,\n );\n }\n }\n }\n\n return raw as ChatSchema;\n}\n\n/**\n * Validates a single page object, collecting its id and question ids.\n *\n * @param raw - The raw page value.\n * @param pageIds - Set of already-seen page ids (mutated in place).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validatePage(\n raw: unknown,\n pageIds: Set<string>,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each page must be a non-null object');\n }\n const page = raw as Record<string, unknown>;\n\n if (typeof page['id'] !== 'string' || !page['id']) {\n throw makeError('Page id must be a non-empty string');\n }\n if (pageIds.has(page['id'] as string)) {\n throw makeError(`Duplicate page id \"${page['id']}\"`);\n }\n pageIds.add(page['id'] as string);\n\n if (!Array.isArray(page['questions'])) {\n throw makeError(`Page \"${page['id']}\" must have a questions array`);\n }\n\n const branching = page['branching'];\n if (!Array.isArray(branching) || (branching as unknown[]).length === 0) {\n throw makeError(`Page \"${page['id']}\" must have a non-empty branching array`);\n }\n\n const lastRule = (branching as unknown[])[branching.length - 1] as Record<string, unknown>;\n if (lastRule['type'] !== 'always') {\n throw makeError(`Page \"${page['id']}\" branching must end with a rule of type \"always\"`);\n }\n\n for (const q of page['questions'] as unknown[]) {\n validateQuestion(q, page['id'] as string, questionIds, rules);\n }\n}\n\n/**\n * Validates a single question object, collecting its id.\n *\n * @param raw - The raw question value.\n * @param pageId - The id of the containing page (for error messages).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validateQuestion(\n raw: unknown,\n pageId: string,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each question must be a non-null object');\n }\n const q = raw as Record<string, unknown>;\n\n if (typeof q['id'] !== 'string' || !q['id']) {\n throw makeError(`In page \"${pageId}\": question id must be a non-empty string`);\n }\n if (questionIds.has(q['id'] as string)) {\n throw makeError(`Duplicate question id \"${q['id']}\"`);\n }\n questionIds.add(q['id'] as string);\n\n if (!VALID_INPUTS.has(q['x-chat-input'] as string)) {\n throw makeError(\n `Question \"${q['id']}\" has invalid x-chat-input \"${q['x-chat-input']}\". ` +\n `Must be one of: text, textarea, dropdown, radio, checkbox`,\n );\n }\n\n const condition = q['x-chat-condition'];\n if (condition && typeof condition === 'object' && !Array.isArray(condition)) {\n const c = condition as Record<string, unknown>;\n if ('ref' in c && typeof c['ref'] === 'string') {\n if (!(c['ref'] in rules)) {\n throw makeError(`Question \"${q['id']}\" x-chat-condition refs unknown rule \"${c['ref']}\"`);\n }\n }\n }\n}\n\n/**\n * Creates a schema validation error object.\n *\n * @param message - Human-readable description of the validation failure.\n * @returns An error object with `code: 'SCHEMA_INVALID'`.\n */\nfunction makeError(message: string): { code: 'SCHEMA_INVALID'; message: string } {\n return { code: 'SCHEMA_INVALID', message };\n}\n","// packages/core/src/session-manager.ts\nimport type { AnswerMap } from './types.js';\n\nexport interface StorageBackend {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n key(index: number): string | null;\n readonly length: number;\n}\n\nexport interface SessionProgress {\n currentPageId: string;\n answers: AnswerMap;\n}\n\nconst KEY_SESSION = 'car:session-id';\nconst keyProgress = (id: string) => `car:progress:${id}`;\nconst keyInput = (sessionId: string, questionId: string) => `car:input:${sessionId}:${questionId}`;\n\n/**\n * What `ChatController` needs from session storage. `SessionManager` keeps it in a\n * synchronous Web Storage object; `AdapterSessionManager` keeps it in a `StorageAdapter`.\n */\nexport interface SessionStore {\n readonly isStorageAvailable: boolean;\n getSessionId(): string;\n saveProgress(currentPageId: string, answers: AnswerMap): void;\n loadProgress(): SessionProgress | null;\n saveInput(questionId: string, value: string | string[]): void;\n loadInput(questionId: string): string | string[] | null;\n clearAll(): void;\n /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */\n dispose?(): void;\n}\n\nexport class SessionManager implements SessionStore {\n private readonly sessionId: string;\n private readonly available: boolean;\n\n constructor(private readonly storage: StorageBackend = globalThis.localStorage) {\n this.available = this.checkStorage();\n this.sessionId = this.loadOrCreateSessionId();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n if (!this.available) return;\n this.storage.setItem(keyProgress(this.sessionId), JSON.stringify({ currentPageId, answers }));\n }\n\n loadProgress(): SessionProgress | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyProgress(this.sessionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as SessionProgress;\n } catch {\n return null;\n }\n }\n\n saveInput(questionId: string, value: string | string[]): void {\n if (!this.available) return;\n this.storage.setItem(keyInput(this.sessionId, questionId), JSON.stringify(value));\n }\n\n loadInput(questionId: string): string | string[] | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyInput(this.sessionId, questionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as string | string[];\n } catch {\n return null;\n }\n }\n\n clearAll(): void {\n if (!this.available) return;\n const progressKey = keyProgress(this.sessionId);\n const inputPrefix = `car:input:${this.sessionId}:`;\n const keysToRemove: string[] = [progressKey];\n\n for (let i = 0; i < this.storage.length; i++) {\n const k = this.storage.key(i);\n if (k?.startsWith(inputPrefix)) keysToRemove.push(k);\n }\n\n for (const k of keysToRemove) {\n this.storage.removeItem(k);\n }\n }\n\n private loadOrCreateSessionId(): string {\n if (!this.available) return crypto.randomUUID();\n const existing = this.storage.getItem(KEY_SESSION);\n if (existing) return existing;\n const id = crypto.randomUUID();\n this.storage.setItem(KEY_SESSION, id);\n return id;\n }\n\n private checkStorage(): boolean {\n try {\n const probe = '__car_probe__';\n this.storage.setItem(probe, '1');\n this.storage.removeItem(probe);\n return true;\n } catch {\n return false;\n }\n }\n}\n","// packages/core/src/chat-controller.ts\n\nimport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nimport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nimport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nimport { RuleEngine } from './rule-engine.js';\nimport { parseSchema } from './schema-parser.js';\nimport { SessionManager, type SessionStore } from './session-manager.js';\nimport type {\n AnswerMap,\n ChatControllerConfig,\n ChatError,\n ChatEvent,\n ChatOptions,\n ChatPage,\n ChatQuestion,\n ChatSchema,\n ErrorLogResult,\n RouteEntry,\n SubmitOutcome,\n} from './types.js';\n\ntype EventName = 'page:submit' | 'form:complete';\ntype Listener = (event: ChatEvent) => void;\n\nconst DEFAULT_OPTIONS: ChatOptions = {\n userResumable: true,\n localStorage: true,\n autoComplete: true,\n allowEdit: false,\n autoHideQuestions: 0,\n autoHideAnswers: 0,\n hideAnimation: 'fade',\n typingSpeed: 0,\n thinkingDelay: 0,\n thinkingText: '',\n showContinueIcon: true,\n};\n\n/** Copies an object without its undefined values, so they cannot hide a default. */\nfunction stripUndefined<T extends object>(source: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(source).filter(([, value]) => value !== undefined),\n ) as Partial<T>;\n}\n\n/**\n * Central orchestrator for the chat-and-react form flow.\n * Drives page navigation, branching, session persistence, and event emission.\n */\nexport class ChatController {\n private schema: ChatSchema | null = null;\n private options: ChatOptions = { ...DEFAULT_OPTIONS };\n private readonly config: Required<ChatControllerConfig>;\n private readonly session: SessionStore;\n private readonly ruleEngine = new RuleEngine();\n private currentPage: ChatPage | null = null;\n private allAnswers: AnswerMap = {};\n private visitIndex = 0;\n /** Pages submitted in this session, in visit order. Used for editing earlier answers. */\n private route: RouteEntry[] = [];\n /** Index in `route` of the page being edited, or null when not editing. */\n private editIndex: number | null = null;\n /** The page the user was on before they started editing an earlier one. */\n private resumePageId: string | null = null;\n private readonly listeners = new Map<EventName, Set<Listener>>();\n\n /**\n * @param config - Controller configuration including schema and optional adapters.\n * @param sessionManager - Optional session manager for test injection.\n */\n constructor(config: ChatControllerConfig, sessionManager?: SessionStore) {\n this.config = {\n inputAdapter: config.inputAdapter ?? new IdentityInputAdapter(),\n outputAdapter: config.outputAdapter ?? new BrowserEventAdapter(),\n errorAdapter: config.errorAdapter ?? new ConsoleErrorAdapter(),\n schema: config.schema,\n options: config.options ?? {},\n };\n this.session = sessionManager ?? new SessionManager();\n }\n\n /**\n * Initialises the controller: parses the schema, resolves options,\n * restores any saved session, and positions to the first page.\n */\n start(): void {\n let raw: unknown;\n try {\n raw = this.config.inputAdapter.transform(this.config.schema);\n } catch (cause) {\n this.handleFatal({ code: 'ADAPTER_ERROR', message: 'InputAdapter.transform() threw', cause });\n }\n\n let schema: ChatSchema;\n try {\n schema = parseSchema(raw);\n } catch (e) {\n this.handleFatal(e as ChatError);\n }\n this.schema = schema;\n\n if (!this.session.isStorageAvailable) {\n this.logError({\n code: 'STORAGE_UNAVAILABLE',\n message: 'localStorage unavailable; running in memory-only mode',\n });\n }\n\n // Defaults, then the schema, then overrides passed in by the host (for\n // example element attributes).\n const schemaOpts = schema['x-chat-options'] ?? {};\n this.options = {\n ...DEFAULT_OPTIONS,\n ...stripUndefined(schemaOpts),\n ...stripUndefined(this.config.options),\n };\n\n if (this.options.userResumable) {\n const saved = this.session.loadProgress();\n if (saved) {\n const page = schema['x-chat-pages'].find((p) => p.id === saved.currentPageId);\n if (page) {\n this.allAnswers = saved.answers;\n this.currentPage = page;\n return;\n }\n }\n }\n\n this.currentPage = schema['x-chat-pages'][0];\n }\n\n /**\n * Returns the page currently being presented to the user, or null if complete.\n */\n getCurrentPage(): ChatPage | null {\n return this.currentPage;\n }\n\n /**\n * Returns the subset of questions on the current page that pass their\n * `x-chat-condition` rule (if any). Questions without a condition are always visible.\n */\n getVisibleQuestions(): ChatQuestion[] {\n if (!this.currentPage) return [];\n return this.currentPage.questions.filter((q) => this.isQuestionVisible(q));\n }\n\n /**\n * Returns the resolved options for the current session (schema options merged with defaults).\n */\n getOptions(): ChatOptions {\n return { ...this.options };\n }\n\n /**\n * Returns the pages the user has submitted in this session, in visit order.\n * A session restored from storage starts with an empty route.\n */\n getRoute(): RouteEntry[] {\n return this.route.map((entry) => ({ pageId: entry.pageId, answers: { ...entry.answers } }));\n }\n\n /**\n * Returns every answer given so far, for pre-filling inputs.\n */\n getAnswers(): AnswerMap {\n return { ...this.allAnswers };\n }\n\n /**\n * Re-opens a page the user already submitted so its answers can be changed.\n * Call `submitPage` afterwards to save the new answers. Not available once the\n * form is complete.\n *\n * @param routeIndex - Position of the page in `getRoute()`.\n * @returns true when the page was re-opened.\n */\n reopenPage(routeIndex: number): boolean {\n if (!this.schema || !this.currentPage) return false;\n const entry = this.route[routeIndex];\n if (!entry) return false;\n const page = this.schema['x-chat-pages'].find((p) => p.id === entry.pageId);\n if (!page) return false;\n\n // Remember where the user was, but only the first time: editing another\n // earlier page before finishing this edit must still return to that page.\n if (this.editIndex === null) this.resumePageId = this.currentPage.id;\n this.editIndex = routeIndex;\n this.currentPage = page;\n return true;\n }\n\n /**\n * Submits the answers for the current page and advances the session.\n * - Accumulates answers into `allAnswers`.\n * - Emits `page:submit` (suppressed when `userResumable` is false).\n * - Resolves branching to find the next page.\n * - Emits `form:complete` and clears session when `**FORM_COMPLETED**` is reached.\n *\n * When the page was re-opened with `reopenPage`, the new answers replace the\n * old ones and `page:submit` is emitted with `edited: true`. If the page's\n * branching still leads to the same next page, the form returns to the page the\n * user was on. If it leads somewhere else, the pages that followed it are\n * dropped, along with their answers, and the form continues from the new page.\n *\n * @param pageAnswers - Answers collected for the current page.\n */\n submitPage(pageAnswers: AnswerMap): SubmitOutcome {\n if (!this.currentPage || !this.schema) return 'advanced';\n\n const pageId = this.currentPage.id;\n const currentVisitIndex = this.visitIndex;\n const editedIndex = this.editIndex;\n const isEdit = editedIndex !== null;\n\n let outcome: SubmitOutcome = 'advanced';\n let nextPageId: string;\n let resumePageId: string | null = null;\n\n if (isEdit) {\n const entry = this.route[editedIndex];\n // Replace the old answers for this page, including any question that is\n // no longer visible after the change.\n const remaining = { ...this.allAnswers };\n for (const questionId of Object.keys(entry.answers)) delete remaining[questionId];\n this.allAnswers = { ...remaining, ...pageAnswers };\n entry.answers = { ...pageAnswers };\n\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n const expectedNextPageId = this.route[editedIndex + 1]?.pageId ?? this.resumePageId;\n\n if (nextPageId === expectedNextPageId) {\n outcome = 'edit-resumed';\n resumePageId = this.resumePageId;\n } else {\n outcome = 'edit-rerouted';\n // The route changed: forget the pages that followed and their answers.\n for (const later of this.route.slice(editedIndex + 1)) {\n for (const questionId of Object.keys(later.answers)) delete this.allAnswers[questionId];\n }\n this.route.length = editedIndex + 1;\n }\n this.editIndex = null;\n this.resumePageId = null;\n } else {\n this.allAnswers = { ...this.allAnswers, ...pageAnswers };\n this.route.push({ pageId, answers: { ...pageAnswers } });\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n }\n\n if (this.options.userResumable) {\n const progressPageId =\n outcome === 'edit-resumed' && resumePageId\n ? resumePageId\n : nextPageId === '**FORM_COMPLETED**'\n ? pageId\n : nextPageId;\n this.session.saveProgress(progressPageId, this.allAnswers);\n\n const submitEvent: ChatEvent = {\n type: 'page:submit',\n sessionId: this.session.getSessionId(),\n pageId,\n visitIndex: currentVisitIndex,\n answers: pageAnswers,\n ...(isEdit ? { edited: true } : {}),\n };\n this.safeEmit(submitEvent);\n this.emit('page:submit', submitEvent);\n }\n\n this.visitIndex++;\n\n if (outcome === 'edit-resumed' && resumePageId) {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === resumePageId) ?? null;\n return outcome;\n }\n\n if (nextPageId === '**FORM_COMPLETED**') {\n this.session.clearAll();\n const completeEvent: ChatEvent = {\n type: 'form:complete',\n sessionId: this.session.getSessionId(),\n allAnswers: this.allAnswers,\n };\n this.safeEmit(completeEvent);\n this.emit('form:complete', completeEvent);\n this.currentPage = null;\n } else {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === nextPageId) ?? null;\n }\n return outcome;\n }\n\n /**\n * Registers an event listener for the given event type.\n *\n * @param event - The event to listen for.\n * @param listener - The callback to invoke when the event fires.\n */\n on(event: EventName, listener: Listener): void {\n let listenersForEvent = this.listeners.get(event);\n if (!listenersForEvent) {\n listenersForEvent = new Set();\n this.listeners.set(event, listenersForEvent);\n }\n listenersForEvent.add(listener);\n }\n\n /**\n * Removes a previously registered event listener.\n *\n * @param event - The event type to deregister from.\n * @param listener - The specific listener to remove.\n */\n off(event: EventName, listener: Listener): void {\n this.listeners.get(event)?.delete(listener);\n }\n\n /**\n * Tears down the controller: removes all listeners and nulls internal state.\n */\n destroy(): void {\n this.session.dispose?.();\n this.listeners.clear();\n this.currentPage = null;\n this.schema = null;\n }\n\n /**\n * Evaluates whether a question should be shown based on its `x-chat-condition`.\n * Returns true when no condition is present. On evaluation failure, logs and returns false.\n */\n private isQuestionVisible(question: ChatQuestion): boolean {\n const condition = question['x-chat-condition'];\n if (!condition) return true;\n\n const rules = this.schema?.['x-chat-rules'] ?? {};\n let rule: Record<string, unknown>;\n\n if ('ref' in condition && typeof (condition as { ref?: unknown }).ref === 'string') {\n const ref = (condition as { ref: string }).ref;\n rule = rules[ref];\n if (!rule) return false;\n } else {\n rule = condition as Record<string, unknown>;\n }\n\n try {\n return this.ruleEngine.evaluate(rule, this.answersForRules());\n } catch (e) {\n this.logError(e as ChatError);\n return false;\n }\n }\n\n /**\n * The answers question conditions are checked against. While an earlier page is\n * being edited, that page's own answers are left out, so its questions are\n * shown exactly as they were the first time the page was asked.\n */\n private answersForRules(): AnswerMap {\n if (this.editIndex === null) return this.allAnswers;\n const rest = { ...this.allAnswers };\n for (const questionId of Object.keys(this.route[this.editIndex].answers))\n delete rest[questionId];\n return rest;\n }\n\n /**\n * Walks the branching rules top-to-bottom. Returns the first match's destination_id.\n * Falls back to `**FORM_COMPLETED**` if nothing matches (should not happen with valid schemas).\n */\n private resolveBranching(page: ChatPage, answers: AnswerMap): string {\n for (const rule of page.branching) {\n if (rule.type === 'always') return rule.destination_id;\n if (rule.type === 'if' && answers[rule.form_element] === rule.value) {\n return rule.destination_id;\n }\n }\n return '**FORM_COMPLETED**';\n }\n\n /**\n * Calls `outputAdapter.emit()` and swallows adapter errors (logging them instead).\n */\n private safeEmit(event: ChatEvent): void {\n try {\n this.config.outputAdapter.emit(event);\n } catch (cause) {\n this.logError({\n code: 'ADAPTER_ERROR',\n message: `OutputAdapter.emit() threw for ${event.type}`,\n cause,\n });\n }\n }\n\n /**\n * Notifies all registered listeners for the given event.\n */\n private emit(event: EventName, payload: ChatEvent): void {\n this.listeners.get(event)?.forEach((l) => {\n l(payload);\n });\n }\n\n /**\n * Logs the error via the error adapter and then throws it.\n * Used for unrecoverable errors (schema invalid, adapter failure on start).\n */\n private handleFatal(error: ChatError): never {\n this.logError(error);\n throw error;\n }\n\n /**\n * Sends the error to the error adapter without waiting for it. If the adapter\n * reports a failure, rejects, or throws, that is written to `console.error` so\n * the original error is never lost and the form keeps running.\n */\n private logError(entry: ChatError): void {\n const reportFailure = (reason: unknown): void => {\n console.error('[chat-and-react] ErrorLogAdapter failed to record an error:', reason, entry);\n };\n try {\n // `Promise.resolve` also tolerates older custom adapters that return nothing.\n void Promise.resolve(this.config.errorAdapter.log(entry)).then(\n (result: ErrorLogResult | undefined): void => {\n if (result?.ok === false) {\n reportFailure(result.error);\n }\n },\n reportFailure,\n );\n } catch (cause) {\n reportFailure(cause);\n }\n }\n}\n","import type {\n StorageAdapter,\n StoredValue,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\nimport type { SessionProgress, SessionStore } from '../session-manager.js';\nimport type { AnswerMap, ChatError } from '../types.js';\n\nconst KEY_SESSION = 'car:session-id';\nconst keyRecord = (sessionId: string): string => `car:session:${sessionId}`;\n\n/** After this many revision conflicts in a row, a save is abandoned and reported. */\nconst MAX_CONFLICT_RETRIES = 3;\n\ntype InputValue = string | string[];\n\n/** Everything saved for one session, stored under a single key. */\ninterface SessionRecord {\n progress: SessionProgress | null;\n inputs: Record<string, InputValue>;\n}\n\nexport interface AdapterSessionManagerOptions {\n /** Called when the adapter fails. The session keeps working from memory. */\n onError?: (error: ChatError) => void;\n /**\n * Called when saved progress changes because of a write made elsewhere, such as\n * another tab. `null` means the session was cleared there.\n */\n onExternalChange?: (progress: SessionProgress | null) => void;\n}\n\nconst emptyRecord = (): SessionRecord => ({ progress: null, inputs: {} });\n\nfunction parseRecord(raw: string): SessionRecord | null {\n try {\n const parsed = JSON.parse(raw) as Partial<SessionRecord>;\n return {\n progress: parsed.progress ?? null,\n inputs: parsed.inputs && typeof parsed.inputs === 'object' ? parsed.inputs : {},\n };\n } catch {\n return null;\n }\n}\n\n/**\n * A session store backed by a `StorageAdapter`. `ChatController` is synchronous, so\n * the saved session is loaded into memory by `create()`, reads come from memory, and\n * writes are saved in the background. Failures are reported through `onError`.\n *\n * Saves carry the revision they were based on. If another tab wrote first, the two\n * are merged: inputs per question, and progress only when this tab saved progress\n * itself, so a tab that only typed cannot overwrite another tab's progress.\n */\nexport class AdapterSessionManager implements SessionStore {\n private record: SessionRecord;\n private revision: string | undefined;\n private readonly dirtyInputs = new Set<string>();\n private dirtyProgress = false;\n private pending = false;\n private persisting: Promise<void> | null = null;\n private readonly signal: AbortSignal = new AbortController().signal;\n private unsubscribe: (() => void) | null = null;\n\n private constructor(\n private readonly adapter: StorageAdapter,\n private readonly sessionId: string,\n private readonly available: boolean,\n initial: StoredValue | null,\n private readonly options: AdapterSessionManagerOptions,\n ) {\n const parsed: SessionRecord | null = initial ? parseRecord(initial.value) : null;\n this.record = parsed ?? emptyRecord();\n this.revision = initial?.revision;\n if (initial && !parsed) {\n this.report(\n `Saved session ${sessionId} could not be read as JSON; starting with an empty session`,\n );\n }\n if (available) {\n this.unsubscribe =\n adapter.subscribe?.(keyRecord(sessionId), (stored) => this.applyRemote(stored)) ?? null;\n }\n }\n\n /** Loads (or creates) the session id and its saved record, then returns the manager. */\n static async create(\n adapter: StorageAdapter,\n options: AdapterSessionManagerOptions = {},\n ): Promise<AdapterSessionManager> {\n const signal: AbortSignal = new AbortController().signal;\n const fail = (message: string): void =>\n options.onError?.({ code: 'STORAGE_UNAVAILABLE', message });\n\n let sessionId: string | null = null;\n let available = true;\n\n const idResult: StoreResult<StoredValue | null> = await adapter.get(KEY_SESSION, { signal });\n if (idResult.ok) {\n sessionId = idResult.value?.value ?? null;\n } else {\n available = false;\n fail(`Could not read the session id: ${idResult.error}. Running in memory-only mode`);\n }\n if (available && sessionId === null) {\n sessionId = crypto.randomUUID();\n const saved: StoreResult<StoredValue> = await adapter.set(KEY_SESSION, sessionId, {\n signal,\n });\n if (!saved.ok) {\n available = false;\n fail(`Could not save the session id: ${saved.error}. Running in memory-only mode`);\n }\n }\n const id: string = sessionId ?? crypto.randomUUID();\n\n let initial: StoredValue | null = null;\n if (available) {\n const recordResult: StoreResult<StoredValue | null> = await adapter.get(keyRecord(id), {\n signal,\n });\n if (recordResult.ok) {\n initial = recordResult.value;\n } else {\n available = false;\n fail(`Could not read saved progress: ${recordResult.error}. Running in memory-only mode`);\n }\n }\n return new AdapterSessionManager(adapter, id, available, initial, options);\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n this.record.progress = { currentPageId, answers };\n this.dirtyProgress = true;\n this.schedulePersist();\n }\n\n loadProgress(): SessionProgress | null {\n return this.record.progress;\n }\n\n saveInput(questionId: string, value: InputValue): void {\n this.record.inputs[questionId] = value;\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n loadInput(questionId: string): InputValue | null {\n return this.record.inputs[questionId] ?? null;\n }\n\n clearAll(): void {\n this.record = emptyRecord();\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.pending = false;\n if (!this.available) return;\n void this.removeRecord();\n }\n\n dispose(): void {\n this.unsubscribe?.();\n this.unsubscribe = null;\n }\n\n /** Resolves when every save made so far has finished. Mostly for tests. */\n async flush(): Promise<void> {\n while (this.persisting) {\n await this.persisting;\n }\n }\n\n private report(message: string, cause?: unknown): void {\n this.options.onError?.({ code: 'STORAGE_UNAVAILABLE', message, cause });\n }\n\n private schedulePersist(): void {\n if (!this.available) return;\n this.pending = true;\n this.persisting ??= this.drain();\n }\n\n /** Saves until nothing is left to save. Never rejects. */\n private async drain(): Promise<void> {\n let conflicts = 0;\n try {\n while (this.pending) {\n this.pending = false;\n const outcome: 'saved' | 'conflict' | 'failed' = await this.persistOnce();\n conflicts = outcome === 'conflict' ? conflicts + 1 : 0;\n if (outcome === 'conflict') {\n if (conflicts >= MAX_CONFLICT_RETRIES) {\n this.report(\n `Gave up saving session ${this.sessionId} after ${conflicts} revision conflicts in a row`,\n );\n break;\n }\n this.pending = true;\n }\n }\n } finally {\n // Cleared in the same tick the loop ends, so a save requested right now starts a new drain.\n this.persisting = null;\n }\n }\n\n private async persistOnce(): Promise<'saved' | 'conflict' | 'failed'> {\n const sentInputs = new Set<string>(this.dirtyInputs);\n const sentProgress: boolean = this.dirtyProgress;\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n\n const result: StoreResult<StoredValue> = await this.adapter.set(\n keyRecord(this.sessionId),\n JSON.stringify(this.record),\n { signal: this.signal, ifRevision: this.revision },\n );\n if (result.ok) {\n this.revision = result.value.revision;\n return 'saved';\n }\n\n // Keep what was not saved so the next attempt includes it.\n for (const questionId of sentInputs) this.dirtyInputs.add(questionId);\n this.dirtyProgress = this.dirtyProgress || sentProgress;\n\n if (result.code !== 'conflict') {\n this.report(`Could not save session ${this.sessionId}: ${result.error}`);\n return 'failed';\n }\n const latest: StoreResult<StoredValue | null> = await this.adapter.get(\n keyRecord(this.sessionId),\n { signal: this.signal },\n );\n if (!latest.ok) {\n this.report(\n `Session ${this.sessionId} changed elsewhere, but the new version could not be read: ${latest.error}`,\n );\n return 'failed';\n }\n this.applyRemote(latest.value);\n return 'conflict';\n }\n\n /** Merges a version written elsewhere with this tab's unsaved changes. */\n private applyRemote(stored: StoredValue | null): void {\n const before: string = JSON.stringify(this.record.progress);\n if (!stored) {\n if (!this.dirtyProgress && this.dirtyInputs.size === 0) {\n this.record = emptyRecord();\n }\n this.revision = undefined;\n } else {\n const remote: SessionRecord | null = parseRecord(stored.value);\n if (!remote) {\n this.report(\n `Session ${this.sessionId} was changed elsewhere to a value that is not valid JSON; ignoring it`,\n );\n this.revision = stored.revision;\n return;\n }\n const inputs: Record<string, InputValue> = { ...remote.inputs };\n for (const questionId of this.dirtyInputs) {\n const mine: InputValue | undefined = this.record.inputs[questionId];\n if (mine !== undefined) inputs[questionId] = mine;\n }\n this.record = {\n progress: this.dirtyProgress ? this.record.progress : remote.progress,\n inputs,\n };\n this.revision = stored.revision;\n }\n if (JSON.stringify(this.record.progress) !== before) {\n this.options.onExternalChange?.(this.record.progress);\n }\n }\n\n private async removeRecord(): Promise<void> {\n const key: string = keyRecord(this.sessionId);\n const result: StoreResult<unknown> = this.adapter.remove\n ? await this.adapter.remove(key, { signal: this.signal })\n : await this.adapter.set(key, JSON.stringify(emptyRecord()), { signal: this.signal });\n if (result.ok) {\n this.revision = undefined;\n } else {\n this.report(`Could not clear session ${this.sessionId}: ${result.error}`);\n }\n }\n}\n"],"mappings":"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","names":["KEY_SESSION"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/adapters/browser-event-adapter.ts","../src/adapters/console-error-adapter.ts","../src/adapters/identity-input-adapter.ts","../src/rule-engine.ts","../src/schema-parser.ts","../src/session-manager.ts","../src/chat-controller.ts","../src/storage/adapter-session-manager.ts"],"sourcesContent":["export type {\n StorageAdapter,\n StoredValue,\n StoreErrorCode,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\n// The browser storage adapters live in the storage-adapters package; re-exported here so\n// they can be imported from core. Server-side adapters (SQLite, files) are in the adapters\n// package.\nexport type {\n LocalStorageAdapterOptions,\n SessionStorageAdapterOptions,\n StorageChangeEvent,\n StorageEventTarget,\n WebStorageAdapterOptions,\n WebStorageLike,\n WebStorageName,\n} from '@richardmcquiston01/storage-adapters/browser';\nexport {\n localStorageAdapter,\n sessionStorageAdapter,\n} from '@richardmcquiston01/storage-adapters/browser';\nexport type { IndexedDbAdapterOptions } from '@richardmcquiston01/storage-adapters/indexeddb';\nexport { indexedDbAdapter } from '@richardmcquiston01/storage-adapters/indexeddb';\nexport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nexport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nexport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nexport { ChatController } from './chat-controller.js';\nexport { RuleEngine } from './rule-engine.js';\nexport { parseSchema } from './schema-parser.js';\nexport type { SessionProgress, SessionStore, StorageBackend } from './session-manager.js';\nexport { SessionManager } from './session-manager.js';\nexport type { AdapterSessionManagerOptions } from './storage/adapter-session-manager.js';\nexport { AdapterSessionManager } from './storage/adapter-session-manager.js';\nexport * from './types.js';\n","import type { ChatEvent, OutputAdapter } from '../types.js';\n\nexport class BrowserEventAdapter implements OutputAdapter {\n emit(event: ChatEvent): void {\n if (typeof document === 'undefined') return;\n const name = event.type === 'page:submit' ? 'chat-page-submit' : 'chat-form-complete';\n document.dispatchEvent(new CustomEvent(name, { detail: event, bubbles: true }));\n }\n}\n","import {\n ConsoleErrorAdapter as BaseConsoleErrorAdapter,\n type ConsoleErrorOptions,\n} from '@richardmcquiston01/error-log-adapter';\nimport type { ChatErrorCode } from '../types.js';\n\n/** Writes errors to `console.error`, prefixed with `[chat-and-react]` unless overridden. */\nexport class ConsoleErrorAdapter extends BaseConsoleErrorAdapter<ChatErrorCode> {\n constructor(options: ConsoleErrorOptions = {}) {\n super({ prefix: '[chat-and-react]', ...options });\n }\n}\n","import type { InputAdapter } from '../types.js';\n\nexport class IdentityInputAdapter implements InputAdapter {\n transform(raw: unknown): unknown {\n return raw;\n }\n}\n","import jsonLogic from 'json-logic-js';\nimport type { AnswerMap, JsonLogicRule } from './types.js';\n\n/**\n * Evaluates JSON Logic rules against provided answers.\n * Wraps json-logic-js to provide type-safe rule evaluation.\n */\nexport class RuleEngine {\n /**\n * Evaluates a JSON Logic rule against an answer map.\n * @param rule The JSON Logic rule to evaluate\n * @param answers The answer map to evaluate the rule against\n * @returns The boolean result of the rule evaluation\n * @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails\n */\n evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean {\n try {\n // json-logic-js apply() returns any; cast to boolean\n return Boolean(\n (jsonLogic as { apply: (rule: unknown, data: unknown) => unknown }).apply(rule, answers),\n );\n } catch (cause) {\n throw { code: 'RULE_EVAL_FAILED', message: 'Rule evaluation failed', cause };\n }\n }\n}\n","import type { ChatPage, ChatSchema } from './types.js';\n\nconst VALID_INPUTS = new Set(['text', 'textarea', 'dropdown', 'radio', 'checkbox']);\nconst FORM_COMPLETED = '**FORM_COMPLETED**';\n\n/**\n * Parses and validates an unknown input as a {@link ChatSchema}.\n *\n * @param raw - The raw input to validate.\n * @returns The typed {@link ChatSchema} if valid.\n * @throws `{ code: 'SCHEMA_INVALID', message: string }` on any validation failure.\n */\nexport function parseSchema(raw: unknown): ChatSchema {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Schema must be a non-null object');\n }\n\n const obj = raw as Record<string, unknown>;\n\n if (obj['x-chat-version'] !== '1') {\n throw makeError('x-chat-version must be \"1\"');\n }\n\n if (!Array.isArray(obj['x-chat-pages'])) {\n throw makeError('x-chat-pages must be an array');\n }\n\n const pages = obj['x-chat-pages'] as unknown[];\n const rules = (\n typeof obj['x-chat-rules'] === 'object' && obj['x-chat-rules'] !== null\n ? obj['x-chat-rules']\n : {}\n ) as Record<string, unknown>;\n\n const pageIds = new Set<string>();\n const questionIds = new Set<string>();\n\n for (const page of pages) {\n validatePage(page, pageIds, questionIds, rules);\n }\n\n // Second pass: validate branching destinations now that all page ids are known\n for (const page of pages) {\n const p = page as ChatPage;\n for (const rule of p.branching) {\n if (rule.destination_id !== FORM_COMPLETED && !pageIds.has(rule.destination_id)) {\n throw makeError(\n `branching destination_id \"${rule.destination_id}\" does not reference a known page id`,\n );\n }\n if (rule.type === 'if' && !questionIds.has(rule.form_element)) {\n throw makeError(\n `branching form_element \"${rule.form_element}\" does not reference a known question id`,\n );\n }\n }\n }\n\n return raw as ChatSchema;\n}\n\n/**\n * Validates a single page object, collecting its id and question ids.\n *\n * @param raw - The raw page value.\n * @param pageIds - Set of already-seen page ids (mutated in place).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validatePage(\n raw: unknown,\n pageIds: Set<string>,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each page must be a non-null object');\n }\n const page = raw as Record<string, unknown>;\n\n if (typeof page['id'] !== 'string' || !page['id']) {\n throw makeError('Page id must be a non-empty string');\n }\n if (pageIds.has(page['id'] as string)) {\n throw makeError(`Duplicate page id \"${page['id']}\"`);\n }\n pageIds.add(page['id'] as string);\n\n if (!Array.isArray(page['questions'])) {\n throw makeError(`Page \"${page['id']}\" must have a questions array`);\n }\n\n const branching = page['branching'];\n if (!Array.isArray(branching) || (branching as unknown[]).length === 0) {\n throw makeError(`Page \"${page['id']}\" must have a non-empty branching array`);\n }\n\n const lastRule = (branching as unknown[])[branching.length - 1] as Record<string, unknown>;\n if (lastRule['type'] !== 'always') {\n throw makeError(`Page \"${page['id']}\" branching must end with a rule of type \"always\"`);\n }\n\n for (const q of page['questions'] as unknown[]) {\n validateQuestion(q, page['id'] as string, questionIds, rules);\n }\n}\n\n/**\n * Validates a single question object, collecting its id.\n *\n * @param raw - The raw question value.\n * @param pageId - The id of the containing page (for error messages).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validateQuestion(\n raw: unknown,\n pageId: string,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each question must be a non-null object');\n }\n const q = raw as Record<string, unknown>;\n\n if (typeof q['id'] !== 'string' || !q['id']) {\n throw makeError(`In page \"${pageId}\": question id must be a non-empty string`);\n }\n if (questionIds.has(q['id'] as string)) {\n throw makeError(`Duplicate question id \"${q['id']}\"`);\n }\n questionIds.add(q['id'] as string);\n\n if (!VALID_INPUTS.has(q['x-chat-input'] as string)) {\n throw makeError(\n `Question \"${q['id']}\" has invalid x-chat-input \"${q['x-chat-input']}\". ` +\n `Must be one of: text, textarea, dropdown, radio, checkbox`,\n );\n }\n\n const condition = q['x-chat-condition'];\n if (condition && typeof condition === 'object' && !Array.isArray(condition)) {\n const c = condition as Record<string, unknown>;\n if ('ref' in c && typeof c['ref'] === 'string') {\n if (!(c['ref'] in rules)) {\n throw makeError(`Question \"${q['id']}\" x-chat-condition refs unknown rule \"${c['ref']}\"`);\n }\n }\n }\n}\n\n/**\n * Creates a schema validation error object.\n *\n * @param message - Human-readable description of the validation failure.\n * @returns An error object with `code: 'SCHEMA_INVALID'`.\n */\nfunction makeError(message: string): { code: 'SCHEMA_INVALID'; message: string } {\n return { code: 'SCHEMA_INVALID', message };\n}\n","// packages/core/src/session-manager.ts\nimport type { AnswerMap } from './types.js';\n\nexport interface StorageBackend {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n key(index: number): string | null;\n readonly length: number;\n}\n\nexport interface SessionProgress {\n currentPageId: string;\n answers: AnswerMap;\n}\n\nconst KEY_SESSION = 'car:session-id';\nconst keyProgress = (id: string) => `car:progress:${id}`;\nconst keyInput = (sessionId: string, questionId: string) => `car:input:${sessionId}:${questionId}`;\n\n/**\n * What `ChatController` needs from session storage. `SessionManager` keeps it in a\n * synchronous Web Storage object; `AdapterSessionManager` keeps it in a `StorageAdapter`.\n */\nexport interface SessionStore {\n readonly isStorageAvailable: boolean;\n getSessionId(): string;\n saveProgress(currentPageId: string, answers: AnswerMap): void;\n loadProgress(): SessionProgress | null;\n saveInput(questionId: string, value: string | string[]): void;\n loadInput(questionId: string): string | string[] | null;\n /** Forgets a saved input, such as a draft that has been submitted. */\n clearInput(questionId: string): void;\n clearAll(): void;\n /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */\n dispose?(): void;\n}\n\nexport class SessionManager implements SessionStore {\n private readonly sessionId: string;\n private readonly available: boolean;\n\n constructor(private readonly storage: StorageBackend = globalThis.localStorage) {\n this.available = this.checkStorage();\n this.sessionId = this.loadOrCreateSessionId();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n if (!this.available) return;\n this.storage.setItem(keyProgress(this.sessionId), JSON.stringify({ currentPageId, answers }));\n }\n\n loadProgress(): SessionProgress | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyProgress(this.sessionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as SessionProgress;\n } catch {\n return null;\n }\n }\n\n saveInput(questionId: string, value: string | string[]): void {\n if (!this.available) return;\n this.storage.setItem(keyInput(this.sessionId, questionId), JSON.stringify(value));\n }\n\n loadInput(questionId: string): string | string[] | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyInput(this.sessionId, questionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as string | string[];\n } catch {\n return null;\n }\n }\n\n clearInput(questionId: string): void {\n if (!this.available) return;\n this.storage.removeItem(keyInput(this.sessionId, questionId));\n }\n\n clearAll(): void {\n if (!this.available) return;\n const progressKey = keyProgress(this.sessionId);\n const inputPrefix = `car:input:${this.sessionId}:`;\n const keysToRemove: string[] = [progressKey];\n\n for (let i = 0; i < this.storage.length; i++) {\n const k = this.storage.key(i);\n if (k?.startsWith(inputPrefix)) keysToRemove.push(k);\n }\n\n for (const k of keysToRemove) {\n this.storage.removeItem(k);\n }\n }\n\n private loadOrCreateSessionId(): string {\n if (!this.available) return crypto.randomUUID();\n const existing = this.storage.getItem(KEY_SESSION);\n if (existing) return existing;\n const id = crypto.randomUUID();\n this.storage.setItem(KEY_SESSION, id);\n return id;\n }\n\n private checkStorage(): boolean {\n try {\n const probe = '__car_probe__';\n this.storage.setItem(probe, '1');\n this.storage.removeItem(probe);\n return true;\n } catch {\n return false;\n }\n }\n}\n","// packages/core/src/chat-controller.ts\n\nimport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nimport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nimport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nimport { RuleEngine } from './rule-engine.js';\nimport { parseSchema } from './schema-parser.js';\nimport { SessionManager, type SessionStore } from './session-manager.js';\nimport type {\n AnswerMap,\n ChatControllerConfig,\n ChatError,\n ChatEvent,\n ChatOptions,\n ChatPage,\n ChatQuestion,\n ChatSchema,\n ErrorLogResult,\n RouteEntry,\n SubmitOutcome,\n} from './types.js';\n\ntype EventName = 'page:submit' | 'form:complete';\ntype Listener = (event: ChatEvent) => void;\n\nconst DEFAULT_OPTIONS: ChatOptions = {\n userResumable: true,\n localStorage: true,\n autoComplete: true,\n allowEdit: false,\n autoHideQuestions: 0,\n autoHideAnswers: 0,\n hideAnimation: 'fade',\n typingSpeed: 0,\n thinkingDelay: 0,\n thinkingText: '',\n showContinueIcon: true,\n};\n\n/** Copies an object without its undefined values, so they cannot hide a default. */\nfunction stripUndefined<T extends object>(source: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(source).filter(([, value]) => value !== undefined),\n ) as Partial<T>;\n}\n\n/**\n * Central orchestrator for the chat-and-react form flow.\n * Drives page navigation, branching, session persistence, and event emission.\n */\nexport class ChatController {\n private schema: ChatSchema | null = null;\n private options: ChatOptions = { ...DEFAULT_OPTIONS };\n private readonly config: Required<ChatControllerConfig>;\n private readonly session: SessionStore;\n private readonly ruleEngine = new RuleEngine();\n private currentPage: ChatPage | null = null;\n private allAnswers: AnswerMap = {};\n private visitIndex = 0;\n /** Pages submitted in this session, in visit order. Used for editing earlier answers. */\n private route: RouteEntry[] = [];\n /** Index in `route` of the page being edited, or null when not editing. */\n private editIndex: number | null = null;\n /** The page the user was on before they started editing an earlier one. */\n private resumePageId: string | null = null;\n private readonly listeners = new Map<EventName, Set<Listener>>();\n\n /**\n * @param config - Controller configuration including schema and optional adapters.\n * @param sessionManager - Optional session manager for test injection.\n */\n constructor(config: ChatControllerConfig, sessionManager?: SessionStore) {\n this.config = {\n inputAdapter: config.inputAdapter ?? new IdentityInputAdapter(),\n outputAdapter: config.outputAdapter ?? new BrowserEventAdapter(),\n errorAdapter: config.errorAdapter ?? new ConsoleErrorAdapter(),\n schema: config.schema,\n options: config.options ?? {},\n };\n this.session = sessionManager ?? new SessionManager();\n }\n\n /**\n * Initialises the controller: parses the schema, resolves options,\n * restores any saved session, and positions to the first page.\n */\n start(): void {\n let raw: unknown;\n try {\n raw = this.config.inputAdapter.transform(this.config.schema);\n } catch (cause) {\n this.handleFatal({ code: 'ADAPTER_ERROR', message: 'InputAdapter.transform() threw', cause });\n }\n\n let schema: ChatSchema;\n try {\n schema = parseSchema(raw);\n } catch (e) {\n this.handleFatal(e as ChatError);\n }\n this.schema = schema;\n\n if (!this.session.isStorageAvailable) {\n this.logError({\n code: 'STORAGE_UNAVAILABLE',\n message: 'localStorage unavailable; running in memory-only mode',\n });\n }\n\n // Defaults, then the schema, then overrides passed in by the host (for\n // example element attributes).\n const schemaOpts = schema['x-chat-options'] ?? {};\n this.options = {\n ...DEFAULT_OPTIONS,\n ...stripUndefined(schemaOpts),\n ...stripUndefined(this.config.options),\n };\n\n if (this.options.userResumable) {\n const saved = this.session.loadProgress();\n if (saved) {\n const page = schema['x-chat-pages'].find((p) => p.id === saved.currentPageId);\n if (page) {\n this.allAnswers = saved.answers;\n this.currentPage = page;\n return;\n }\n }\n }\n\n this.currentPage = schema['x-chat-pages'][0];\n }\n\n /**\n * Returns the page currently being presented to the user, or null if complete.\n */\n getCurrentPage(): ChatPage | null {\n return this.currentPage;\n }\n\n /**\n * Returns the subset of questions on the current page that pass their\n * `x-chat-condition` rule (if any). Questions without a condition are always visible.\n */\n getVisibleQuestions(): ChatQuestion[] {\n if (!this.currentPage) return [];\n return this.currentPage.questions.filter((q) => this.isQuestionVisible(q));\n }\n\n /**\n * Returns the resolved options for the current session (schema options merged with defaults).\n */\n getOptions(): ChatOptions {\n return { ...this.options };\n }\n\n /**\n * Returns the pages the user has submitted in this session, in visit order.\n * A session restored from storage starts with an empty route.\n */\n getRoute(): RouteEntry[] {\n return this.route.map((entry) => ({ pageId: entry.pageId, answers: { ...entry.answers } }));\n }\n\n /**\n * Returns every answer given so far, for pre-filling inputs.\n */\n getAnswers(): AnswerMap {\n return { ...this.allAnswers };\n }\n\n /**\n * Re-opens a page the user already submitted so its answers can be changed.\n * Call `submitPage` afterwards to save the new answers. Not available once the\n * form is complete.\n *\n * @param routeIndex - Position of the page in `getRoute()`.\n * @returns true when the page was re-opened.\n */\n reopenPage(routeIndex: number): boolean {\n if (!this.schema || !this.currentPage) return false;\n const entry = this.route[routeIndex];\n if (!entry) return false;\n const page = this.schema['x-chat-pages'].find((p) => p.id === entry.pageId);\n if (!page) return false;\n\n // Remember where the user was, but only the first time: editing another\n // earlier page before finishing this edit must still return to that page.\n if (this.editIndex === null) this.resumePageId = this.currentPage.id;\n this.editIndex = routeIndex;\n this.currentPage = page;\n return true;\n }\n\n /**\n * Submits the answers for the current page and advances the session.\n * - Accumulates answers into `allAnswers`.\n * - Emits `page:submit` (suppressed when `userResumable` is false).\n * - Resolves branching to find the next page.\n * - Emits `form:complete` and clears session when `**FORM_COMPLETED**` is reached.\n *\n * When the page was re-opened with `reopenPage`, the new answers replace the\n * old ones and `page:submit` is emitted with `edited: true`. If the page's\n * branching still leads to the same next page, the form returns to the page the\n * user was on. If it leads somewhere else, the pages that followed it are\n * dropped, along with their answers, and the form continues from the new page.\n *\n * @param pageAnswers - Answers collected for the current page.\n */\n submitPage(pageAnswers: AnswerMap): SubmitOutcome {\n if (!this.currentPage || !this.schema) return 'advanced';\n\n const pageId = this.currentPage.id;\n const currentVisitIndex = this.visitIndex;\n const editedIndex = this.editIndex;\n const isEdit = editedIndex !== null;\n\n let outcome: SubmitOutcome = 'advanced';\n let nextPageId: string;\n let resumePageId: string | null = null;\n\n if (isEdit) {\n const entry = this.route[editedIndex];\n // Replace the old answers for this page, including any question that is\n // no longer visible after the change.\n const remaining = { ...this.allAnswers };\n for (const questionId of Object.keys(entry.answers)) delete remaining[questionId];\n this.allAnswers = { ...remaining, ...pageAnswers };\n entry.answers = { ...pageAnswers };\n\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n const expectedNextPageId = this.route[editedIndex + 1]?.pageId ?? this.resumePageId;\n\n if (nextPageId === expectedNextPageId) {\n outcome = 'edit-resumed';\n resumePageId = this.resumePageId;\n } else {\n outcome = 'edit-rerouted';\n // The route changed: forget the pages that followed and their answers.\n for (const later of this.route.slice(editedIndex + 1)) {\n for (const questionId of Object.keys(later.answers)) delete this.allAnswers[questionId];\n }\n this.route.length = editedIndex + 1;\n }\n this.editIndex = null;\n this.resumePageId = null;\n } else {\n this.allAnswers = { ...this.allAnswers, ...pageAnswers };\n this.route.push({ pageId, answers: { ...pageAnswers } });\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n }\n\n if (this.options.userResumable) {\n const progressPageId =\n outcome === 'edit-resumed' && resumePageId\n ? resumePageId\n : nextPageId === '**FORM_COMPLETED**'\n ? pageId\n : nextPageId;\n this.session.saveProgress(progressPageId, this.allAnswers);\n\n const submitEvent: ChatEvent = {\n type: 'page:submit',\n sessionId: this.session.getSessionId(),\n pageId,\n visitIndex: currentVisitIndex,\n answers: pageAnswers,\n ...(isEdit ? { edited: true } : {}),\n };\n this.safeEmit(submitEvent);\n this.emit('page:submit', submitEvent);\n }\n\n this.visitIndex++;\n\n if (outcome === 'edit-resumed' && resumePageId) {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === resumePageId) ?? null;\n return outcome;\n }\n\n if (nextPageId === '**FORM_COMPLETED**') {\n this.session.clearAll();\n const completeEvent: ChatEvent = {\n type: 'form:complete',\n sessionId: this.session.getSessionId(),\n allAnswers: this.allAnswers,\n };\n this.safeEmit(completeEvent);\n this.emit('form:complete', completeEvent);\n this.currentPage = null;\n } else {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === nextPageId) ?? null;\n }\n return outcome;\n }\n\n /**\n * Registers an event listener for the given event type.\n *\n * @param event - The event to listen for.\n * @param listener - The callback to invoke when the event fires.\n */\n on(event: EventName, listener: Listener): void {\n let listenersForEvent = this.listeners.get(event);\n if (!listenersForEvent) {\n listenersForEvent = new Set();\n this.listeners.set(event, listenersForEvent);\n }\n listenersForEvent.add(listener);\n }\n\n /**\n * Removes a previously registered event listener.\n *\n * @param event - The event type to deregister from.\n * @param listener - The specific listener to remove.\n */\n off(event: EventName, listener: Listener): void {\n this.listeners.get(event)?.delete(listener);\n }\n\n /**\n * Tears down the controller: removes all listeners and nulls internal state.\n */\n destroy(): void {\n this.session.dispose?.();\n this.listeners.clear();\n this.currentPage = null;\n this.schema = null;\n }\n\n /**\n * Evaluates whether a question should be shown based on its `x-chat-condition`.\n * Returns true when no condition is present. On evaluation failure, logs and returns false.\n */\n private isQuestionVisible(question: ChatQuestion): boolean {\n const condition = question['x-chat-condition'];\n if (!condition) return true;\n\n const rules = this.schema?.['x-chat-rules'] ?? {};\n let rule: Record<string, unknown>;\n\n if ('ref' in condition && typeof (condition as { ref?: unknown }).ref === 'string') {\n const ref = (condition as { ref: string }).ref;\n rule = rules[ref];\n if (!rule) return false;\n } else {\n rule = condition as Record<string, unknown>;\n }\n\n try {\n return this.ruleEngine.evaluate(rule, this.answersForRules());\n } catch (e) {\n this.logError(e as ChatError);\n return false;\n }\n }\n\n /**\n * The answers question conditions are checked against. While an earlier page is\n * being edited, that page's own answers are left out, so its questions are\n * shown exactly as they were the first time the page was asked.\n */\n private answersForRules(): AnswerMap {\n if (this.editIndex === null) return this.allAnswers;\n const rest = { ...this.allAnswers };\n for (const questionId of Object.keys(this.route[this.editIndex].answers))\n delete rest[questionId];\n return rest;\n }\n\n /**\n * Walks the branching rules top-to-bottom. Returns the first match's destination_id.\n * Falls back to `**FORM_COMPLETED**` if nothing matches (should not happen with valid schemas).\n */\n private resolveBranching(page: ChatPage, answers: AnswerMap): string {\n for (const rule of page.branching) {\n if (rule.type === 'always') return rule.destination_id;\n if (rule.type === 'if' && answers[rule.form_element] === rule.value) {\n return rule.destination_id;\n }\n }\n return '**FORM_COMPLETED**';\n }\n\n /**\n * Calls `outputAdapter.emit()` and swallows adapter errors (logging them instead).\n */\n private safeEmit(event: ChatEvent): void {\n try {\n this.config.outputAdapter.emit(event);\n } catch (cause) {\n this.logError({\n code: 'ADAPTER_ERROR',\n message: `OutputAdapter.emit() threw for ${event.type}`,\n cause,\n });\n }\n }\n\n /**\n * Notifies all registered listeners for the given event.\n */\n private emit(event: EventName, payload: ChatEvent): void {\n this.listeners.get(event)?.forEach((l) => {\n l(payload);\n });\n }\n\n /**\n * Logs the error via the error adapter and then throws it.\n * Used for unrecoverable errors (schema invalid, adapter failure on start).\n */\n private handleFatal(error: ChatError): never {\n this.logError(error);\n throw error;\n }\n\n /**\n * Sends the error to the error adapter without waiting for it. If the adapter\n * reports a failure, rejects, or throws, that is written to `console.error` so\n * the original error is never lost and the form keeps running.\n */\n private logError(entry: ChatError): void {\n const reportFailure = (reason: unknown): void => {\n console.error('[chat-and-react] ErrorLogAdapter failed to record an error:', reason, entry);\n };\n try {\n // `Promise.resolve` also tolerates older custom adapters that return nothing.\n void Promise.resolve(this.config.errorAdapter.log(entry)).then(\n (result: ErrorLogResult | undefined): void => {\n if (result?.ok === false) {\n reportFailure(result.error);\n }\n },\n reportFailure,\n );\n } catch (cause) {\n reportFailure(cause);\n }\n }\n}\n","import type {\n StorageAdapter,\n StoredValue,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\nimport type { SessionProgress, SessionStore } from '../session-manager.js';\nimport type { AnswerMap, ChatError } from '../types.js';\n\nconst KEY_SESSION = 'car:session-id';\nconst keyRecord = (sessionId: string): string => `car:session:${sessionId}`;\n\n/** After this many revision conflicts in a row, a save is abandoned and reported. */\nconst MAX_CONFLICT_RETRIES = 3;\n\ntype InputValue = string | string[];\n\n/** Everything saved for one session, stored under a single key. */\ninterface SessionRecord {\n progress: SessionProgress | null;\n inputs: Record<string, InputValue>;\n}\n\nexport interface AdapterSessionManagerOptions {\n /** Called when the adapter fails. The session keeps working from memory. */\n onError?: (error: ChatError) => void;\n /**\n * Called when saved progress changes because of a write made elsewhere, such as\n * another tab. `null` means the session was cleared there.\n */\n onExternalChange?: (progress: SessionProgress | null) => void;\n}\n\nconst emptyRecord = (): SessionRecord => ({ progress: null, inputs: {} });\n\nfunction parseRecord(raw: string): SessionRecord | null {\n try {\n const parsed = JSON.parse(raw) as Partial<SessionRecord>;\n return {\n progress: parsed.progress ?? null,\n inputs: parsed.inputs && typeof parsed.inputs === 'object' ? parsed.inputs : {},\n };\n } catch {\n return null;\n }\n}\n\n/**\n * A session store backed by a `StorageAdapter`. `ChatController` is synchronous, so\n * the saved session is loaded into memory by `create()`, reads come from memory, and\n * writes are saved in the background. Failures are reported through `onError`.\n *\n * Saves carry the revision they were based on. If another tab wrote first, the two\n * are merged: inputs per question, and progress only when this tab saved progress\n * itself, so a tab that only typed cannot overwrite another tab's progress.\n */\nexport class AdapterSessionManager implements SessionStore {\n private record: SessionRecord;\n private revision: string | undefined;\n private readonly dirtyInputs = new Set<string>();\n private dirtyProgress = false;\n private pending = false;\n private persisting: Promise<void> | null = null;\n private readonly signal: AbortSignal = new AbortController().signal;\n private unsubscribe: (() => void) | null = null;\n\n private constructor(\n private readonly adapter: StorageAdapter,\n private readonly sessionId: string,\n private readonly available: boolean,\n initial: StoredValue | null,\n private readonly options: AdapterSessionManagerOptions,\n ) {\n const parsed: SessionRecord | null = initial ? parseRecord(initial.value) : null;\n this.record = parsed ?? emptyRecord();\n this.revision = initial?.revision;\n if (initial && !parsed) {\n this.report(\n `Saved session ${sessionId} could not be read as JSON; starting with an empty session`,\n );\n }\n if (available) {\n this.unsubscribe =\n adapter.subscribe?.(keyRecord(sessionId), (stored) => this.applyRemote(stored)) ?? null;\n }\n }\n\n /** Loads (or creates) the session id and its saved record, then returns the manager. */\n static async create(\n adapter: StorageAdapter,\n options: AdapterSessionManagerOptions = {},\n ): Promise<AdapterSessionManager> {\n const signal: AbortSignal = new AbortController().signal;\n const fail = (message: string): void =>\n options.onError?.({ code: 'STORAGE_UNAVAILABLE', message });\n\n let sessionId: string | null = null;\n let available = true;\n\n const idResult: StoreResult<StoredValue | null> = await adapter.get(KEY_SESSION, { signal });\n if (idResult.ok) {\n sessionId = idResult.value?.value ?? null;\n } else {\n available = false;\n fail(`Could not read the session id: ${idResult.error}. Running in memory-only mode`);\n }\n if (available && sessionId === null) {\n sessionId = crypto.randomUUID();\n const saved: StoreResult<StoredValue> = await adapter.set(KEY_SESSION, sessionId, {\n signal,\n });\n if (!saved.ok) {\n available = false;\n fail(`Could not save the session id: ${saved.error}. Running in memory-only mode`);\n }\n }\n const id: string = sessionId ?? crypto.randomUUID();\n\n let initial: StoredValue | null = null;\n if (available) {\n const recordResult: StoreResult<StoredValue | null> = await adapter.get(keyRecord(id), {\n signal,\n });\n if (recordResult.ok) {\n initial = recordResult.value;\n } else {\n available = false;\n fail(`Could not read saved progress: ${recordResult.error}. Running in memory-only mode`);\n }\n }\n return new AdapterSessionManager(adapter, id, available, initial, options);\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n this.record.progress = { currentPageId, answers };\n this.dirtyProgress = true;\n this.schedulePersist();\n }\n\n loadProgress(): SessionProgress | null {\n return this.record.progress;\n }\n\n saveInput(questionId: string, value: InputValue): void {\n this.record.inputs[questionId] = value;\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n loadInput(questionId: string): InputValue | null {\n return this.record.inputs[questionId] ?? null;\n }\n\n clearInput(questionId: string): void {\n if (!(questionId in this.record.inputs)) return;\n delete this.record.inputs[questionId];\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n clearAll(): void {\n this.record = emptyRecord();\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.pending = false;\n if (!this.available) return;\n void this.removeRecord();\n }\n\n dispose(): void {\n this.unsubscribe?.();\n this.unsubscribe = null;\n }\n\n /** Resolves when every save made so far has finished. Mostly for tests. */\n async flush(): Promise<void> {\n while (this.persisting) {\n await this.persisting;\n }\n }\n\n private report(message: string, cause?: unknown): void {\n this.options.onError?.({ code: 'STORAGE_UNAVAILABLE', message, cause });\n }\n\n private schedulePersist(): void {\n if (!this.available) return;\n this.pending = true;\n this.persisting ??= this.drain();\n }\n\n /** Saves until nothing is left to save. Never rejects. */\n private async drain(): Promise<void> {\n let conflicts = 0;\n try {\n while (this.pending) {\n this.pending = false;\n const outcome: 'saved' | 'conflict' | 'failed' = await this.persistOnce();\n conflicts = outcome === 'conflict' ? conflicts + 1 : 0;\n if (outcome === 'conflict') {\n if (conflicts >= MAX_CONFLICT_RETRIES) {\n this.report(\n `Gave up saving session ${this.sessionId} after ${conflicts} revision conflicts in a row`,\n );\n break;\n }\n this.pending = true;\n }\n }\n } finally {\n // Cleared in the same tick the loop ends, so a save requested right now starts a new drain.\n this.persisting = null;\n }\n }\n\n private async persistOnce(): Promise<'saved' | 'conflict' | 'failed'> {\n const sentInputs = new Set<string>(this.dirtyInputs);\n const sentProgress: boolean = this.dirtyProgress;\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n\n const result: StoreResult<StoredValue> = await this.adapter.set(\n keyRecord(this.sessionId),\n JSON.stringify(this.record),\n { signal: this.signal, ifRevision: this.revision },\n );\n if (result.ok) {\n this.revision = result.value.revision;\n return 'saved';\n }\n\n // Keep what was not saved so the next attempt includes it.\n for (const questionId of sentInputs) this.dirtyInputs.add(questionId);\n this.dirtyProgress = this.dirtyProgress || sentProgress;\n\n if (result.code !== 'conflict') {\n this.report(`Could not save session ${this.sessionId}: ${result.error}`);\n return 'failed';\n }\n const latest: StoreResult<StoredValue | null> = await this.adapter.get(\n keyRecord(this.sessionId),\n { signal: this.signal },\n );\n if (!latest.ok) {\n this.report(\n `Session ${this.sessionId} changed elsewhere, but the new version could not be read: ${latest.error}`,\n );\n return 'failed';\n }\n this.applyRemote(latest.value);\n return 'conflict';\n }\n\n /** Merges a version written elsewhere with this tab's unsaved changes. */\n private applyRemote(stored: StoredValue | null): void {\n const before: string = JSON.stringify(this.record.progress);\n if (!stored) {\n if (!this.dirtyProgress && this.dirtyInputs.size === 0) {\n this.record = emptyRecord();\n }\n this.revision = undefined;\n } else {\n const remote: SessionRecord | null = parseRecord(stored.value);\n if (!remote) {\n this.report(\n `Session ${this.sessionId} was changed elsewhere to a value that is not valid JSON; ignoring it`,\n );\n this.revision = stored.revision;\n return;\n }\n const inputs: Record<string, InputValue> = { ...remote.inputs };\n for (const questionId of this.dirtyInputs) {\n const mine: InputValue | undefined = this.record.inputs[questionId];\n // An input that is dirty but missing here was cleared on this tab.\n if (mine !== undefined) {\n inputs[questionId] = mine;\n } else {\n delete inputs[questionId];\n }\n }\n this.record = {\n progress: this.dirtyProgress ? this.record.progress : remote.progress,\n inputs,\n };\n this.revision = stored.revision;\n }\n if (JSON.stringify(this.record.progress) !== before) {\n this.options.onExternalChange?.(this.record.progress);\n }\n }\n\n private async removeRecord(): Promise<void> {\n const key: string = keyRecord(this.sessionId);\n const result: StoreResult<unknown> = this.adapter.remove\n ? await this.adapter.remove(key, { signal: this.signal })\n : await this.adapter.set(key, JSON.stringify(emptyRecord()), { signal: this.signal });\n if (result.ok) {\n this.revision = undefined;\n } else {\n this.report(`Could not clear session ${this.sessionId}: ${result.error}`);\n }\n }\n}\n"],"mappings":"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","names":["KEY_SESSION"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@richardmcquiston01/chat-and-react-core",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Headless controller, JSON Schema validation, and session management for chat-and-react — a framework-agnostic conversational form library.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Richard McQuiston <richard.mcquiston01@gmail.com>",