@richardmcquiston01/chat-and-react-core 0.10.0 → 0.11.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
@@ -191,6 +191,7 @@ function makeError(message) {
191
191
  var KEY_SESSION = "car:session-id";
192
192
  var keyProgress = (id) => `car:progress:${id}`;
193
193
  var keyInput = (sessionId, questionId) => `car:input:${sessionId}:${questionId}`;
194
+ var keyCompleted = (sessionId) => `car:completed:${sessionId}`;
194
195
  var SessionManager = class {
195
196
  constructor(storage = globalThis.localStorage) {
196
197
  this.storage = storage;
@@ -235,11 +236,21 @@ var SessionManager = class {
235
236
  if (!this.available) return;
236
237
  this.storage.removeItem(keyInput(this.sessionId, questionId));
237
238
  }
239
+ complete() {
240
+ this.clearAll();
241
+ if (!this.available) return;
242
+ this.storage.setItem(keyCompleted(this.sessionId), String(Date.now()));
243
+ }
244
+ completedAt() {
245
+ if (!this.available) return null;
246
+ const completedAt = Number(this.storage.getItem(keyCompleted(this.sessionId)));
247
+ return Number.isFinite(completedAt) && completedAt > 0 ? completedAt : null;
248
+ }
238
249
  clearAll() {
239
250
  if (!this.available) return;
240
251
  const progressKey = keyProgress(this.sessionId);
241
252
  const inputPrefix = `car:input:${this.sessionId}:`;
242
- const keysToRemove = [progressKey];
253
+ const keysToRemove = [progressKey, keyCompleted(this.sessionId)];
243
254
  for (let i = 0; i < this.storage.length; i++) {
244
255
  const k = this.storage.key(i);
245
256
  if (k?.startsWith(inputPrefix)) keysToRemove.push(k);
@@ -479,7 +490,11 @@ var ChatController = class {
479
490
  return outcome;
480
491
  }
481
492
  if (nextPageId === "**FORM_COMPLETED**") {
482
- this.session.clearAll();
493
+ if (this.session.complete) {
494
+ this.session.complete();
495
+ } else {
496
+ this.session.clearAll();
497
+ }
483
498
  const completeEvent = {
484
499
  type: "form:complete",
485
500
  sessionId: this.session.getSessionId(),
@@ -637,7 +652,8 @@ function parseRecord(raw) {
637
652
  const parsed = JSON.parse(raw);
638
653
  return {
639
654
  progress: parsed.progress ?? null,
640
- inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {}
655
+ inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {},
656
+ ...typeof parsed.completedAt === "number" ? { completedAt: parsed.completedAt } : {}
641
657
  };
642
658
  } catch {
643
659
  return null;
@@ -651,6 +667,8 @@ var AdapterSessionManager = class _AdapterSessionManager {
651
667
  this.options = options;
652
668
  this.dirtyInputs = /* @__PURE__ */ new Set();
653
669
  this.dirtyProgress = false;
670
+ /** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
671
+ this.resetPending = false;
654
672
  this.pending = false;
655
673
  this.persisting = null;
656
674
  this.signal = new AbortController().signal;
@@ -733,10 +751,21 @@ var AdapterSessionManager = class _AdapterSessionManager {
733
751
  this.dirtyInputs.add(questionId);
734
752
  this.schedulePersist();
735
753
  }
754
+ complete() {
755
+ this.record = { progress: null, inputs: {}, completedAt: Date.now() };
756
+ this.dirtyInputs.clear();
757
+ this.dirtyProgress = true;
758
+ this.resetPending = true;
759
+ this.schedulePersist();
760
+ }
761
+ completedAt() {
762
+ return this.record.completedAt ?? null;
763
+ }
736
764
  clearAll() {
737
765
  this.record = emptyRecord();
738
766
  this.dirtyInputs.clear();
739
767
  this.dirtyProgress = false;
768
+ this.resetPending = false;
740
769
  this.pending = false;
741
770
  if (!this.available) return;
742
771
  void this.removeRecord();
@@ -784,8 +813,10 @@ var AdapterSessionManager = class _AdapterSessionManager {
784
813
  async persistOnce() {
785
814
  const sentInputs = new Set(this.dirtyInputs);
786
815
  const sentProgress = this.dirtyProgress;
816
+ const sentReset = this.resetPending;
787
817
  this.dirtyInputs.clear();
788
818
  this.dirtyProgress = false;
819
+ this.resetPending = false;
789
820
  const result = await this.adapter.set(
790
821
  keyRecord(this.sessionId),
791
822
  JSON.stringify(this.record),
@@ -797,6 +828,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
797
828
  }
798
829
  for (const questionId of sentInputs) this.dirtyInputs.add(questionId);
799
830
  this.dirtyProgress = this.dirtyProgress || sentProgress;
831
+ this.resetPending = this.resetPending || sentReset;
800
832
  if (result.code !== "conflict") {
801
833
  this.report(`Could not save session ${this.sessionId}: ${result.error}`);
802
834
  return "failed";
@@ -831,7 +863,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
831
863
  this.revision = stored.revision;
832
864
  return;
833
865
  }
834
- const inputs = { ...remote.inputs };
866
+ const inputs = this.resetPending ? {} : { ...remote.inputs };
835
867
  for (const questionId of this.dirtyInputs) {
836
868
  const mine = this.record.inputs[questionId];
837
869
  if (mine !== void 0) {
@@ -840,9 +872,11 @@ var AdapterSessionManager = class _AdapterSessionManager {
840
872
  delete inputs[questionId];
841
873
  }
842
874
  }
875
+ const completedAt = Math.max(this.record.completedAt ?? 0, remote.completedAt ?? 0) || void 0;
843
876
  this.record = {
844
877
  progress: this.dirtyProgress ? this.record.progress : remote.progress,
845
- inputs
878
+ inputs,
879
+ ...completedAt !== void 0 ? { completedAt } : {}
846
880
  };
847
881
  this.revision = stored.revision;
848
882
  }
@@ -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 /** 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"]}
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}`;\nconst keyCompleted = (sessionId: string) => `car:completed:${sessionId}`;\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 /**\n * Called when the form is finished. Clears the saved session like `clearAll()` and also records\n * that it was completed, so another tab that is still open can tell \"finished\" from \"cleared\".\n * A store without it is cleared with `clearAll()`.\n */\n complete?(): void;\n /** When `complete()` was last called, in any tab (milliseconds since the epoch), or null. */\n completedAt?(): number | null;\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 complete(): void {\n this.clearAll();\n if (!this.available) return;\n this.storage.setItem(keyCompleted(this.sessionId), String(Date.now()));\n }\n\n completedAt(): number | null {\n if (!this.available) return null;\n const completedAt = Number(this.storage.getItem(keyCompleted(this.sessionId)));\n return Number.isFinite(completedAt) && completedAt > 0 ? completedAt : null;\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, keyCompleted(this.sessionId)];\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 if (this.session.complete) {\n this.session.complete();\n } else {\n this.session.clearAll();\n }\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 /** When the form was finished (milliseconds since the epoch), if it has been. */\n completedAt?: number;\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 ...(typeof parsed.completedAt === 'number' ? { completedAt: parsed.completedAt } : {}),\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 /** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */\n private resetPending = 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 complete(): void {\n this.record = { progress: null, inputs: {}, completedAt: Date.now() };\n this.dirtyInputs.clear();\n this.dirtyProgress = true;\n this.resetPending = true;\n this.schedulePersist();\n }\n\n completedAt(): number | null {\n return this.record.completedAt ?? null;\n }\n\n clearAll(): void {\n this.record = emptyRecord();\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.resetPending = 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 const sentReset: boolean = this.resetPending;\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.resetPending = 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 this.resetPending = this.resetPending || sentReset;\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 // A finished form leaves nothing of another tab's behind; otherwise the two are merged.\n const inputs: Record<string, InputValue> = this.resetPending ? {} : { ...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 const completedAt: number | undefined =\n Math.max(this.record.completedAt ?? 0, remote.completedAt ?? 0) || undefined;\n this.record = {\n progress: this.dirtyProgress ? this.record.progress : remote.progress,\n inputs,\n ...(completedAt !== undefined ? { completedAt } : {}),\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
@@ -173,6 +173,14 @@ interface SessionStore {
173
173
  /** Forgets a saved input, such as a draft that has been submitted. */
174
174
  clearInput(questionId: string): void;
175
175
  clearAll(): void;
176
+ /**
177
+ * Called when the form is finished. Clears the saved session like `clearAll()` and also records
178
+ * that it was completed, so another tab that is still open can tell "finished" from "cleared".
179
+ * A store without it is cleared with `clearAll()`.
180
+ */
181
+ complete?(): void;
182
+ /** When `complete()` was last called, in any tab (milliseconds since the epoch), or null. */
183
+ completedAt?(): number | null;
176
184
  /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */
177
185
  dispose?(): void;
178
186
  }
@@ -188,6 +196,8 @@ declare class SessionManager implements SessionStore {
188
196
  saveInput(questionId: string, value: string | string[]): void;
189
197
  loadInput(questionId: string): string | string[] | null;
190
198
  clearInput(questionId: string): void;
199
+ complete(): void;
200
+ completedAt(): number | null;
191
201
  clearAll(): void;
192
202
  private loadOrCreateSessionId;
193
203
  private checkStorage;
@@ -379,6 +389,8 @@ declare class AdapterSessionManager implements SessionStore {
379
389
  private revision;
380
390
  private readonly dirtyInputs;
381
391
  private dirtyProgress;
392
+ /** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
393
+ private resetPending;
382
394
  private pending;
383
395
  private persisting;
384
396
  private readonly signal;
@@ -393,6 +405,8 @@ declare class AdapterSessionManager implements SessionStore {
393
405
  saveInput(questionId: string, value: InputValue): void;
394
406
  loadInput(questionId: string): InputValue | null;
395
407
  clearInput(questionId: string): void;
408
+ complete(): void;
409
+ completedAt(): number | null;
396
410
  clearAll(): void;
397
411
  dispose(): void;
398
412
  /** Resolves when every save made so far has finished. Mostly for tests. */
package/dist/index.d.ts CHANGED
@@ -173,6 +173,14 @@ interface SessionStore {
173
173
  /** Forgets a saved input, such as a draft that has been submitted. */
174
174
  clearInput(questionId: string): void;
175
175
  clearAll(): void;
176
+ /**
177
+ * Called when the form is finished. Clears the saved session like `clearAll()` and also records
178
+ * that it was completed, so another tab that is still open can tell "finished" from "cleared".
179
+ * A store without it is cleared with `clearAll()`.
180
+ */
181
+ complete?(): void;
182
+ /** When `complete()` was last called, in any tab (milliseconds since the epoch), or null. */
183
+ completedAt?(): number | null;
176
184
  /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */
177
185
  dispose?(): void;
178
186
  }
@@ -188,6 +196,8 @@ declare class SessionManager implements SessionStore {
188
196
  saveInput(questionId: string, value: string | string[]): void;
189
197
  loadInput(questionId: string): string | string[] | null;
190
198
  clearInput(questionId: string): void;
199
+ complete(): void;
200
+ completedAt(): number | null;
191
201
  clearAll(): void;
192
202
  private loadOrCreateSessionId;
193
203
  private checkStorage;
@@ -379,6 +389,8 @@ declare class AdapterSessionManager implements SessionStore {
379
389
  private revision;
380
390
  private readonly dirtyInputs;
381
391
  private dirtyProgress;
392
+ /** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
393
+ private resetPending;
382
394
  private pending;
383
395
  private persisting;
384
396
  private readonly signal;
@@ -393,6 +405,8 @@ declare class AdapterSessionManager implements SessionStore {
393
405
  saveInput(questionId: string, value: InputValue): void;
394
406
  loadInput(questionId: string): InputValue | null;
395
407
  clearInput(questionId: string): void;
408
+ complete(): void;
409
+ completedAt(): number | null;
396
410
  clearAll(): void;
397
411
  dispose(): void;
398
412
  /** Resolves when every save made so far has finished. Mostly for tests. */
package/dist/index.mjs CHANGED
@@ -152,6 +152,7 @@ function makeError(message) {
152
152
  var KEY_SESSION = "car:session-id";
153
153
  var keyProgress = (id) => `car:progress:${id}`;
154
154
  var keyInput = (sessionId, questionId) => `car:input:${sessionId}:${questionId}`;
155
+ var keyCompleted = (sessionId) => `car:completed:${sessionId}`;
155
156
  var SessionManager = class {
156
157
  constructor(storage = globalThis.localStorage) {
157
158
  this.storage = storage;
@@ -196,11 +197,21 @@ var SessionManager = class {
196
197
  if (!this.available) return;
197
198
  this.storage.removeItem(keyInput(this.sessionId, questionId));
198
199
  }
200
+ complete() {
201
+ this.clearAll();
202
+ if (!this.available) return;
203
+ this.storage.setItem(keyCompleted(this.sessionId), String(Date.now()));
204
+ }
205
+ completedAt() {
206
+ if (!this.available) return null;
207
+ const completedAt = Number(this.storage.getItem(keyCompleted(this.sessionId)));
208
+ return Number.isFinite(completedAt) && completedAt > 0 ? completedAt : null;
209
+ }
199
210
  clearAll() {
200
211
  if (!this.available) return;
201
212
  const progressKey = keyProgress(this.sessionId);
202
213
  const inputPrefix = `car:input:${this.sessionId}:`;
203
- const keysToRemove = [progressKey];
214
+ const keysToRemove = [progressKey, keyCompleted(this.sessionId)];
204
215
  for (let i = 0; i < this.storage.length; i++) {
205
216
  const k = this.storage.key(i);
206
217
  if (k?.startsWith(inputPrefix)) keysToRemove.push(k);
@@ -440,7 +451,11 @@ var ChatController = class {
440
451
  return outcome;
441
452
  }
442
453
  if (nextPageId === "**FORM_COMPLETED**") {
443
- this.session.clearAll();
454
+ if (this.session.complete) {
455
+ this.session.complete();
456
+ } else {
457
+ this.session.clearAll();
458
+ }
444
459
  const completeEvent = {
445
460
  type: "form:complete",
446
461
  sessionId: this.session.getSessionId(),
@@ -598,7 +613,8 @@ function parseRecord(raw) {
598
613
  const parsed = JSON.parse(raw);
599
614
  return {
600
615
  progress: parsed.progress ?? null,
601
- inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {}
616
+ inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {},
617
+ ...typeof parsed.completedAt === "number" ? { completedAt: parsed.completedAt } : {}
602
618
  };
603
619
  } catch {
604
620
  return null;
@@ -612,6 +628,8 @@ var AdapterSessionManager = class _AdapterSessionManager {
612
628
  this.options = options;
613
629
  this.dirtyInputs = /* @__PURE__ */ new Set();
614
630
  this.dirtyProgress = false;
631
+ /** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
632
+ this.resetPending = false;
615
633
  this.pending = false;
616
634
  this.persisting = null;
617
635
  this.signal = new AbortController().signal;
@@ -694,10 +712,21 @@ var AdapterSessionManager = class _AdapterSessionManager {
694
712
  this.dirtyInputs.add(questionId);
695
713
  this.schedulePersist();
696
714
  }
715
+ complete() {
716
+ this.record = { progress: null, inputs: {}, completedAt: Date.now() };
717
+ this.dirtyInputs.clear();
718
+ this.dirtyProgress = true;
719
+ this.resetPending = true;
720
+ this.schedulePersist();
721
+ }
722
+ completedAt() {
723
+ return this.record.completedAt ?? null;
724
+ }
697
725
  clearAll() {
698
726
  this.record = emptyRecord();
699
727
  this.dirtyInputs.clear();
700
728
  this.dirtyProgress = false;
729
+ this.resetPending = false;
701
730
  this.pending = false;
702
731
  if (!this.available) return;
703
732
  void this.removeRecord();
@@ -745,8 +774,10 @@ var AdapterSessionManager = class _AdapterSessionManager {
745
774
  async persistOnce() {
746
775
  const sentInputs = new Set(this.dirtyInputs);
747
776
  const sentProgress = this.dirtyProgress;
777
+ const sentReset = this.resetPending;
748
778
  this.dirtyInputs.clear();
749
779
  this.dirtyProgress = false;
780
+ this.resetPending = false;
750
781
  const result = await this.adapter.set(
751
782
  keyRecord(this.sessionId),
752
783
  JSON.stringify(this.record),
@@ -758,6 +789,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
758
789
  }
759
790
  for (const questionId of sentInputs) this.dirtyInputs.add(questionId);
760
791
  this.dirtyProgress = this.dirtyProgress || sentProgress;
792
+ this.resetPending = this.resetPending || sentReset;
761
793
  if (result.code !== "conflict") {
762
794
  this.report(`Could not save session ${this.sessionId}: ${result.error}`);
763
795
  return "failed";
@@ -792,7 +824,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
792
824
  this.revision = stored.revision;
793
825
  return;
794
826
  }
795
- const inputs = { ...remote.inputs };
827
+ const inputs = this.resetPending ? {} : { ...remote.inputs };
796
828
  for (const questionId of this.dirtyInputs) {
797
829
  const mine = this.record.inputs[questionId];
798
830
  if (mine !== void 0) {
@@ -801,9 +833,11 @@ var AdapterSessionManager = class _AdapterSessionManager {
801
833
  delete inputs[questionId];
802
834
  }
803
835
  }
836
+ const completedAt = Math.max(this.record.completedAt ?? 0, remote.completedAt ?? 0) || void 0;
804
837
  this.record = {
805
838
  progress: this.dirtyProgress ? this.record.progress : remote.progress,
806
- inputs
839
+ inputs,
840
+ ...completedAt !== void 0 ? { completedAt } : {}
807
841
  };
808
842
  this.revision = stored.revision;
809
843
  }
@@ -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 /** 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"]}
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}`;\nconst keyCompleted = (sessionId: string) => `car:completed:${sessionId}`;\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 /**\n * Called when the form is finished. Clears the saved session like `clearAll()` and also records\n * that it was completed, so another tab that is still open can tell \"finished\" from \"cleared\".\n * A store without it is cleared with `clearAll()`.\n */\n complete?(): void;\n /** When `complete()` was last called, in any tab (milliseconds since the epoch), or null. */\n completedAt?(): number | null;\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 complete(): void {\n this.clearAll();\n if (!this.available) return;\n this.storage.setItem(keyCompleted(this.sessionId), String(Date.now()));\n }\n\n completedAt(): number | null {\n if (!this.available) return null;\n const completedAt = Number(this.storage.getItem(keyCompleted(this.sessionId)));\n return Number.isFinite(completedAt) && completedAt > 0 ? completedAt : null;\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, keyCompleted(this.sessionId)];\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 if (this.session.complete) {\n this.session.complete();\n } else {\n this.session.clearAll();\n }\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 /** When the form was finished (milliseconds since the epoch), if it has been. */\n completedAt?: number;\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 ...(typeof parsed.completedAt === 'number' ? { completedAt: parsed.completedAt } : {}),\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 /** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */\n private resetPending = 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 complete(): void {\n this.record = { progress: null, inputs: {}, completedAt: Date.now() };\n this.dirtyInputs.clear();\n this.dirtyProgress = true;\n this.resetPending = true;\n this.schedulePersist();\n }\n\n completedAt(): number | null {\n return this.record.completedAt ?? null;\n }\n\n clearAll(): void {\n this.record = emptyRecord();\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.resetPending = 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 const sentReset: boolean = this.resetPending;\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.resetPending = 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 this.resetPending = this.resetPending || sentReset;\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 // A finished form leaves nothing of another tab's behind; otherwise the two are merged.\n const inputs: Record<string, InputValue> = this.resetPending ? {} : { ...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 const completedAt: number | undefined =\n Math.max(this.record.completedAt ?? 0, remote.completedAt ?? 0) || undefined;\n this.record = {\n progress: this.dirtyProgress ? this.record.progress : remote.progress,\n inputs,\n ...(completedAt !== undefined ? { completedAt } : {}),\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.10.0",
3
+ "version": "0.11.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>",