@richardmcquiston01/chat-and-react-core 0.10.0 → 0.12.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/README.md CHANGED
@@ -41,7 +41,7 @@ controller.submitPage({ question_id: "answer value" });
41
41
  - `SessionManager` — session ID and progress persistence with an injectable `StorageBackend`
42
42
  - `ChatController` — page-graph navigation, answer accumulation, event emission (`page:submit`, `form:complete`)
43
43
  - Built-in adapters: `IdentityInputAdapter`, `BrowserEventAdapter`, `ConsoleErrorAdapter`
44
- - `AdapterSessionManager` — saves progress through any storage adapter; `localStorageAdapter`, `sessionStorageAdapter` and `indexedDbAdapter` are re-exported from `@richardmcquiston01/storage-adapters`
44
+ - `AdapterSessionManager` — saves progress through any storage adapter; the `localStorageAdapter`, `sessionStorageAdapter` and `indexedDbAdapter` it takes come from `@richardmcquiston01/storage-adapters`
45
45
 
46
46
  ## Full documentation
47
47
 
package/dist/index.cjs CHANGED
@@ -35,16 +35,10 @@ __export(index_exports, {
35
35
  ChatController: () => ChatController,
36
36
  ConsoleErrorAdapter: () => ConsoleErrorAdapter,
37
37
  IdentityInputAdapter: () => IdentityInputAdapter,
38
- RuleEngine: () => RuleEngine,
39
38
  SessionManager: () => SessionManager,
40
- indexedDbAdapter: () => import_indexeddb.indexedDbAdapter,
41
- localStorageAdapter: () => import_browser.localStorageAdapter,
42
- parseSchema: () => parseSchema,
43
- sessionStorageAdapter: () => import_browser.sessionStorageAdapter
39
+ parseSchema: () => parseSchema
44
40
  });
45
41
  module.exports = __toCommonJS(index_exports);
46
- var import_browser = require("@richardmcquiston01/storage-adapters/browser");
47
- var import_indexeddb = require("@richardmcquiston01/storage-adapters/indexeddb");
48
42
 
49
43
  // src/adapters/browser-event-adapter.ts
50
44
  var BrowserEventAdapter = class {
@@ -191,6 +185,7 @@ function makeError(message) {
191
185
  var KEY_SESSION = "car:session-id";
192
186
  var keyProgress = (id) => `car:progress:${id}`;
193
187
  var keyInput = (sessionId, questionId) => `car:input:${sessionId}:${questionId}`;
188
+ var keyCompleted = (sessionId) => `car:completed:${sessionId}`;
194
189
  var SessionManager = class {
195
190
  constructor(storage = globalThis.localStorage) {
196
191
  this.storage = storage;
@@ -235,11 +230,21 @@ var SessionManager = class {
235
230
  if (!this.available) return;
236
231
  this.storage.removeItem(keyInput(this.sessionId, questionId));
237
232
  }
233
+ complete() {
234
+ this.clearAll();
235
+ if (!this.available) return;
236
+ this.storage.setItem(keyCompleted(this.sessionId), String(Date.now()));
237
+ }
238
+ completedAt() {
239
+ if (!this.available) return null;
240
+ const completedAt = Number(this.storage.getItem(keyCompleted(this.sessionId)));
241
+ return Number.isFinite(completedAt) && completedAt > 0 ? completedAt : null;
242
+ }
238
243
  clearAll() {
239
244
  if (!this.available) return;
240
245
  const progressKey = keyProgress(this.sessionId);
241
246
  const inputPrefix = `car:input:${this.sessionId}:`;
242
- const keysToRemove = [progressKey];
247
+ const keysToRemove = [progressKey, keyCompleted(this.sessionId)];
243
248
  for (let i = 0; i < this.storage.length; i++) {
244
249
  const k = this.storage.key(i);
245
250
  if (k?.startsWith(inputPrefix)) keysToRemove.push(k);
@@ -479,7 +484,11 @@ var ChatController = class {
479
484
  return outcome;
480
485
  }
481
486
  if (nextPageId === "**FORM_COMPLETED**") {
482
- this.session.clearAll();
487
+ if (this.session.complete) {
488
+ this.session.complete();
489
+ } else {
490
+ this.session.clearAll();
491
+ }
483
492
  const completeEvent = {
484
493
  type: "form:complete",
485
494
  sessionId: this.session.getSessionId(),
@@ -637,7 +646,8 @@ function parseRecord(raw) {
637
646
  const parsed = JSON.parse(raw);
638
647
  return {
639
648
  progress: parsed.progress ?? null,
640
- inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {}
649
+ inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {},
650
+ ...typeof parsed.completedAt === "number" ? { completedAt: parsed.completedAt } : {}
641
651
  };
642
652
  } catch {
643
653
  return null;
@@ -651,6 +661,8 @@ var AdapterSessionManager = class _AdapterSessionManager {
651
661
  this.options = options;
652
662
  this.dirtyInputs = /* @__PURE__ */ new Set();
653
663
  this.dirtyProgress = false;
664
+ /** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
665
+ this.resetPending = false;
654
666
  this.pending = false;
655
667
  this.persisting = null;
656
668
  this.signal = new AbortController().signal;
@@ -733,10 +745,21 @@ var AdapterSessionManager = class _AdapterSessionManager {
733
745
  this.dirtyInputs.add(questionId);
734
746
  this.schedulePersist();
735
747
  }
748
+ complete() {
749
+ this.record = { progress: null, inputs: {}, completedAt: Date.now() };
750
+ this.dirtyInputs.clear();
751
+ this.dirtyProgress = true;
752
+ this.resetPending = true;
753
+ this.schedulePersist();
754
+ }
755
+ completedAt() {
756
+ return this.record.completedAt ?? null;
757
+ }
736
758
  clearAll() {
737
759
  this.record = emptyRecord();
738
760
  this.dirtyInputs.clear();
739
761
  this.dirtyProgress = false;
762
+ this.resetPending = false;
740
763
  this.pending = false;
741
764
  if (!this.available) return;
742
765
  void this.removeRecord();
@@ -784,8 +807,10 @@ var AdapterSessionManager = class _AdapterSessionManager {
784
807
  async persistOnce() {
785
808
  const sentInputs = new Set(this.dirtyInputs);
786
809
  const sentProgress = this.dirtyProgress;
810
+ const sentReset = this.resetPending;
787
811
  this.dirtyInputs.clear();
788
812
  this.dirtyProgress = false;
813
+ this.resetPending = false;
789
814
  const result = await this.adapter.set(
790
815
  keyRecord(this.sessionId),
791
816
  JSON.stringify(this.record),
@@ -797,6 +822,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
797
822
  }
798
823
  for (const questionId of sentInputs) this.dirtyInputs.add(questionId);
799
824
  this.dirtyProgress = this.dirtyProgress || sentProgress;
825
+ this.resetPending = this.resetPending || sentReset;
800
826
  if (result.code !== "conflict") {
801
827
  this.report(`Could not save session ${this.sessionId}: ${result.error}`);
802
828
  return "failed";
@@ -831,7 +857,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
831
857
  this.revision = stored.revision;
832
858
  return;
833
859
  }
834
- const inputs = { ...remote.inputs };
860
+ const inputs = this.resetPending ? {} : { ...remote.inputs };
835
861
  for (const questionId of this.dirtyInputs) {
836
862
  const mine = this.record.inputs[questionId];
837
863
  if (mine !== void 0) {
@@ -840,9 +866,11 @@ var AdapterSessionManager = class _AdapterSessionManager {
840
866
  delete inputs[questionId];
841
867
  }
842
868
  }
869
+ const completedAt = Math.max(this.record.completedAt ?? 0, remote.completedAt ?? 0) || void 0;
843
870
  this.record = {
844
871
  progress: this.dirtyProgress ? this.record.progress : remote.progress,
845
- inputs
872
+ inputs,
873
+ ...completedAt !== void 0 ? { completedAt } : {}
846
874
  };
847
875
  this.revision = stored.revision;
848
876
  }
@@ -867,11 +895,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
867
895
  ChatController,
868
896
  ConsoleErrorAdapter,
869
897
  IdentityInputAdapter,
870
- RuleEngine,
871
898
  SessionManager,
872
- indexedDbAdapter,
873
- localStorageAdapter,
874
- parseSchema,
875
- sessionStorageAdapter
899
+ parseSchema
876
900
  });
877
901
  //# sourceMappingURL=index.cjs.map
@@ -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 { StorageAdapter } from '@richardmcquiston01/storage-adapters';\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 { parseSchema } from './schema-parser.js';\nexport type { SessionProgress, SessionStore } 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 type {\n AnswerMap,\n BranchingAlways,\n BranchingIf,\n BranchingRule,\n ChatConditionRef,\n ChatControllerConfig,\n ChatError,\n ChatErrorCode,\n ChatEvent,\n ChatInputType,\n ChatOptions,\n ChatPage,\n ChatQuestion,\n ChatSchema,\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n FormCompleteEvent,\n HideAnimation,\n InputAdapter,\n JsonLogicRule,\n OutputAdapter,\n PageSubmitEvent,\n} 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
@@ -1,7 +1,5 @@
1
1
  import { StorageAdapter } from '@richardmcquiston01/storage-adapters';
2
- export { StorageAdapter, StoreErrorCode, StoreResult, StoredValue } from '@richardmcquiston01/storage-adapters';
3
- export { LocalStorageAdapterOptions, SessionStorageAdapterOptions, StorageChangeEvent, StorageEventTarget, WebStorageAdapterOptions, WebStorageLike, WebStorageName, localStorageAdapter, sessionStorageAdapter } from '@richardmcquiston01/storage-adapters/browser';
4
- export { IndexedDbAdapterOptions, indexedDbAdapter } from '@richardmcquiston01/storage-adapters/indexeddb';
2
+ export { StorageAdapter } from '@richardmcquiston01/storage-adapters';
5
3
  import { ErrorLogEntry, ErrorLogAdapter as ErrorLogAdapter$1, ConsoleErrorAdapter as ConsoleErrorAdapter$1, ConsoleErrorOptions } from '@richardmcquiston01/error-log-adapter';
6
4
  export { ErrorLogEntry, ErrorLogResult } from '@richardmcquiston01/error-log-adapter';
7
5
 
@@ -173,6 +171,14 @@ interface SessionStore {
173
171
  /** Forgets a saved input, such as a draft that has been submitted. */
174
172
  clearInput(questionId: string): void;
175
173
  clearAll(): void;
174
+ /**
175
+ * Called when the form is finished. Clears the saved session like `clearAll()` and also records
176
+ * that it was completed, so another tab that is still open can tell "finished" from "cleared".
177
+ * A store without it is cleared with `clearAll()`.
178
+ */
179
+ complete?(): void;
180
+ /** When `complete()` was last called, in any tab (milliseconds since the epoch), or null. */
181
+ completedAt?(): number | null;
176
182
  /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */
177
183
  dispose?(): void;
178
184
  }
@@ -188,6 +194,8 @@ declare class SessionManager implements SessionStore {
188
194
  saveInput(questionId: string, value: string | string[]): void;
189
195
  loadInput(questionId: string): string | string[] | null;
190
196
  clearInput(questionId: string): void;
197
+ complete(): void;
198
+ completedAt(): number | null;
191
199
  clearAll(): void;
192
200
  private loadOrCreateSessionId;
193
201
  private checkStorage;
@@ -327,21 +335,6 @@ declare class ChatController {
327
335
  private logError;
328
336
  }
329
337
 
330
- /**
331
- * Evaluates JSON Logic rules against provided answers.
332
- * Wraps json-logic-js to provide type-safe rule evaluation.
333
- */
334
- declare class RuleEngine {
335
- /**
336
- * Evaluates a JSON Logic rule against an answer map.
337
- * @param rule The JSON Logic rule to evaluate
338
- * @param answers The answer map to evaluate the rule against
339
- * @returns The boolean result of the rule evaluation
340
- * @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails
341
- */
342
- evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean;
343
- }
344
-
345
338
  /**
346
339
  * Parses and validates an unknown input as a {@link ChatSchema}.
347
340
  *
@@ -379,6 +372,8 @@ declare class AdapterSessionManager implements SessionStore {
379
372
  private revision;
380
373
  private readonly dirtyInputs;
381
374
  private dirtyProgress;
375
+ /** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
376
+ private resetPending;
382
377
  private pending;
383
378
  private persisting;
384
379
  private readonly signal;
@@ -393,6 +388,8 @@ declare class AdapterSessionManager implements SessionStore {
393
388
  saveInput(questionId: string, value: InputValue): void;
394
389
  loadInput(questionId: string): InputValue | null;
395
390
  clearInput(questionId: string): void;
391
+ complete(): void;
392
+ completedAt(): number | null;
396
393
  clearAll(): void;
397
394
  dispose(): void;
398
395
  /** Resolves when every save made so far has finished. Mostly for tests. */
@@ -407,4 +404,4 @@ declare class AdapterSessionManager implements SessionStore {
407
404
  private removeRecord;
408
405
  }
409
406
 
410
- export { AdapterSessionManager, type AdapterSessionManagerOptions, type AnswerMap, type BranchingAlways, type BranchingIf, type BranchingRule, BrowserEventAdapter, type ChatConditionRef, ChatController, type ChatControllerConfig, type ChatError, type ChatErrorCode, type ChatEvent, type ChatInputType, type ChatOptions, type ChatPage, type ChatQuestion, type ChatSchema, ConsoleErrorAdapter, type ErrorLogAdapter, type FormCompleteEvent, type HideAnimation, IdentityInputAdapter, type InputAdapter, type JsonLogicRule, type OutputAdapter, type PageSubmitEvent, type RouteEntry, RuleEngine, SessionManager, type SessionProgress, type SessionStore, type StorageBackend, type SubmitOutcome, parseSchema };
407
+ export { AdapterSessionManager, type AdapterSessionManagerOptions, type AnswerMap, type BranchingAlways, type BranchingIf, type BranchingRule, BrowserEventAdapter, type ChatConditionRef, ChatController, type ChatControllerConfig, type ChatError, type ChatErrorCode, type ChatEvent, type ChatInputType, type ChatOptions, type ChatPage, type ChatQuestion, type ChatSchema, ConsoleErrorAdapter, type ErrorLogAdapter, type FormCompleteEvent, type HideAnimation, IdentityInputAdapter, type InputAdapter, type JsonLogicRule, type OutputAdapter, type PageSubmitEvent, SessionManager, type SessionProgress, type SessionStore, parseSchema };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  import { StorageAdapter } from '@richardmcquiston01/storage-adapters';
2
- export { StorageAdapter, StoreErrorCode, StoreResult, StoredValue } from '@richardmcquiston01/storage-adapters';
3
- export { LocalStorageAdapterOptions, SessionStorageAdapterOptions, StorageChangeEvent, StorageEventTarget, WebStorageAdapterOptions, WebStorageLike, WebStorageName, localStorageAdapter, sessionStorageAdapter } from '@richardmcquiston01/storage-adapters/browser';
4
- export { IndexedDbAdapterOptions, indexedDbAdapter } from '@richardmcquiston01/storage-adapters/indexeddb';
2
+ export { StorageAdapter } from '@richardmcquiston01/storage-adapters';
5
3
  import { ErrorLogEntry, ErrorLogAdapter as ErrorLogAdapter$1, ConsoleErrorAdapter as ConsoleErrorAdapter$1, ConsoleErrorOptions } from '@richardmcquiston01/error-log-adapter';
6
4
  export { ErrorLogEntry, ErrorLogResult } from '@richardmcquiston01/error-log-adapter';
7
5
 
@@ -173,6 +171,14 @@ interface SessionStore {
173
171
  /** Forgets a saved input, such as a draft that has been submitted. */
174
172
  clearInput(questionId: string): void;
175
173
  clearAll(): void;
174
+ /**
175
+ * Called when the form is finished. Clears the saved session like `clearAll()` and also records
176
+ * that it was completed, so another tab that is still open can tell "finished" from "cleared".
177
+ * A store without it is cleared with `clearAll()`.
178
+ */
179
+ complete?(): void;
180
+ /** When `complete()` was last called, in any tab (milliseconds since the epoch), or null. */
181
+ completedAt?(): number | null;
176
182
  /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */
177
183
  dispose?(): void;
178
184
  }
@@ -188,6 +194,8 @@ declare class SessionManager implements SessionStore {
188
194
  saveInput(questionId: string, value: string | string[]): void;
189
195
  loadInput(questionId: string): string | string[] | null;
190
196
  clearInput(questionId: string): void;
197
+ complete(): void;
198
+ completedAt(): number | null;
191
199
  clearAll(): void;
192
200
  private loadOrCreateSessionId;
193
201
  private checkStorage;
@@ -327,21 +335,6 @@ declare class ChatController {
327
335
  private logError;
328
336
  }
329
337
 
330
- /**
331
- * Evaluates JSON Logic rules against provided answers.
332
- * Wraps json-logic-js to provide type-safe rule evaluation.
333
- */
334
- declare class RuleEngine {
335
- /**
336
- * Evaluates a JSON Logic rule against an answer map.
337
- * @param rule The JSON Logic rule to evaluate
338
- * @param answers The answer map to evaluate the rule against
339
- * @returns The boolean result of the rule evaluation
340
- * @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails
341
- */
342
- evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean;
343
- }
344
-
345
338
  /**
346
339
  * Parses and validates an unknown input as a {@link ChatSchema}.
347
340
  *
@@ -379,6 +372,8 @@ declare class AdapterSessionManager implements SessionStore {
379
372
  private revision;
380
373
  private readonly dirtyInputs;
381
374
  private dirtyProgress;
375
+ /** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
376
+ private resetPending;
382
377
  private pending;
383
378
  private persisting;
384
379
  private readonly signal;
@@ -393,6 +388,8 @@ declare class AdapterSessionManager implements SessionStore {
393
388
  saveInput(questionId: string, value: InputValue): void;
394
389
  loadInput(questionId: string): InputValue | null;
395
390
  clearInput(questionId: string): void;
391
+ complete(): void;
392
+ completedAt(): number | null;
396
393
  clearAll(): void;
397
394
  dispose(): void;
398
395
  /** Resolves when every save made so far has finished. Mostly for tests. */
@@ -407,4 +404,4 @@ declare class AdapterSessionManager implements SessionStore {
407
404
  private removeRecord;
408
405
  }
409
406
 
410
- export { AdapterSessionManager, type AdapterSessionManagerOptions, type AnswerMap, type BranchingAlways, type BranchingIf, type BranchingRule, BrowserEventAdapter, type ChatConditionRef, ChatController, type ChatControllerConfig, type ChatError, type ChatErrorCode, type ChatEvent, type ChatInputType, type ChatOptions, type ChatPage, type ChatQuestion, type ChatSchema, ConsoleErrorAdapter, type ErrorLogAdapter, type FormCompleteEvent, type HideAnimation, IdentityInputAdapter, type InputAdapter, type JsonLogicRule, type OutputAdapter, type PageSubmitEvent, type RouteEntry, RuleEngine, SessionManager, type SessionProgress, type SessionStore, type StorageBackend, type SubmitOutcome, parseSchema };
407
+ export { AdapterSessionManager, type AdapterSessionManagerOptions, type AnswerMap, type BranchingAlways, type BranchingIf, type BranchingRule, BrowserEventAdapter, type ChatConditionRef, ChatController, type ChatControllerConfig, type ChatError, type ChatErrorCode, type ChatEvent, type ChatInputType, type ChatOptions, type ChatPage, type ChatQuestion, type ChatSchema, ConsoleErrorAdapter, type ErrorLogAdapter, type FormCompleteEvent, type HideAnimation, IdentityInputAdapter, type InputAdapter, type JsonLogicRule, type OutputAdapter, type PageSubmitEvent, SessionManager, type SessionProgress, type SessionStore, parseSchema };
package/dist/index.mjs CHANGED
@@ -1,10 +1,3 @@
1
- // src/index.ts
2
- import {
3
- localStorageAdapter,
4
- sessionStorageAdapter
5
- } from "@richardmcquiston01/storage-adapters/browser";
6
- import { indexedDbAdapter } from "@richardmcquiston01/storage-adapters/indexeddb";
7
-
8
1
  // src/adapters/browser-event-adapter.ts
9
2
  var BrowserEventAdapter = class {
10
3
  emit(event) {
@@ -152,6 +145,7 @@ function makeError(message) {
152
145
  var KEY_SESSION = "car:session-id";
153
146
  var keyProgress = (id) => `car:progress:${id}`;
154
147
  var keyInput = (sessionId, questionId) => `car:input:${sessionId}:${questionId}`;
148
+ var keyCompleted = (sessionId) => `car:completed:${sessionId}`;
155
149
  var SessionManager = class {
156
150
  constructor(storage = globalThis.localStorage) {
157
151
  this.storage = storage;
@@ -196,11 +190,21 @@ var SessionManager = class {
196
190
  if (!this.available) return;
197
191
  this.storage.removeItem(keyInput(this.sessionId, questionId));
198
192
  }
193
+ complete() {
194
+ this.clearAll();
195
+ if (!this.available) return;
196
+ this.storage.setItem(keyCompleted(this.sessionId), String(Date.now()));
197
+ }
198
+ completedAt() {
199
+ if (!this.available) return null;
200
+ const completedAt = Number(this.storage.getItem(keyCompleted(this.sessionId)));
201
+ return Number.isFinite(completedAt) && completedAt > 0 ? completedAt : null;
202
+ }
199
203
  clearAll() {
200
204
  if (!this.available) return;
201
205
  const progressKey = keyProgress(this.sessionId);
202
206
  const inputPrefix = `car:input:${this.sessionId}:`;
203
- const keysToRemove = [progressKey];
207
+ const keysToRemove = [progressKey, keyCompleted(this.sessionId)];
204
208
  for (let i = 0; i < this.storage.length; i++) {
205
209
  const k = this.storage.key(i);
206
210
  if (k?.startsWith(inputPrefix)) keysToRemove.push(k);
@@ -440,7 +444,11 @@ var ChatController = class {
440
444
  return outcome;
441
445
  }
442
446
  if (nextPageId === "**FORM_COMPLETED**") {
443
- this.session.clearAll();
447
+ if (this.session.complete) {
448
+ this.session.complete();
449
+ } else {
450
+ this.session.clearAll();
451
+ }
444
452
  const completeEvent = {
445
453
  type: "form:complete",
446
454
  sessionId: this.session.getSessionId(),
@@ -598,7 +606,8 @@ function parseRecord(raw) {
598
606
  const parsed = JSON.parse(raw);
599
607
  return {
600
608
  progress: parsed.progress ?? null,
601
- inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {}
609
+ inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {},
610
+ ...typeof parsed.completedAt === "number" ? { completedAt: parsed.completedAt } : {}
602
611
  };
603
612
  } catch {
604
613
  return null;
@@ -612,6 +621,8 @@ var AdapterSessionManager = class _AdapterSessionManager {
612
621
  this.options = options;
613
622
  this.dirtyInputs = /* @__PURE__ */ new Set();
614
623
  this.dirtyProgress = false;
624
+ /** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
625
+ this.resetPending = false;
615
626
  this.pending = false;
616
627
  this.persisting = null;
617
628
  this.signal = new AbortController().signal;
@@ -694,10 +705,21 @@ var AdapterSessionManager = class _AdapterSessionManager {
694
705
  this.dirtyInputs.add(questionId);
695
706
  this.schedulePersist();
696
707
  }
708
+ complete() {
709
+ this.record = { progress: null, inputs: {}, completedAt: Date.now() };
710
+ this.dirtyInputs.clear();
711
+ this.dirtyProgress = true;
712
+ this.resetPending = true;
713
+ this.schedulePersist();
714
+ }
715
+ completedAt() {
716
+ return this.record.completedAt ?? null;
717
+ }
697
718
  clearAll() {
698
719
  this.record = emptyRecord();
699
720
  this.dirtyInputs.clear();
700
721
  this.dirtyProgress = false;
722
+ this.resetPending = false;
701
723
  this.pending = false;
702
724
  if (!this.available) return;
703
725
  void this.removeRecord();
@@ -745,8 +767,10 @@ var AdapterSessionManager = class _AdapterSessionManager {
745
767
  async persistOnce() {
746
768
  const sentInputs = new Set(this.dirtyInputs);
747
769
  const sentProgress = this.dirtyProgress;
770
+ const sentReset = this.resetPending;
748
771
  this.dirtyInputs.clear();
749
772
  this.dirtyProgress = false;
773
+ this.resetPending = false;
750
774
  const result = await this.adapter.set(
751
775
  keyRecord(this.sessionId),
752
776
  JSON.stringify(this.record),
@@ -758,6 +782,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
758
782
  }
759
783
  for (const questionId of sentInputs) this.dirtyInputs.add(questionId);
760
784
  this.dirtyProgress = this.dirtyProgress || sentProgress;
785
+ this.resetPending = this.resetPending || sentReset;
761
786
  if (result.code !== "conflict") {
762
787
  this.report(`Could not save session ${this.sessionId}: ${result.error}`);
763
788
  return "failed";
@@ -792,7 +817,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
792
817
  this.revision = stored.revision;
793
818
  return;
794
819
  }
795
- const inputs = { ...remote.inputs };
820
+ const inputs = this.resetPending ? {} : { ...remote.inputs };
796
821
  for (const questionId of this.dirtyInputs) {
797
822
  const mine = this.record.inputs[questionId];
798
823
  if (mine !== void 0) {
@@ -801,9 +826,11 @@ var AdapterSessionManager = class _AdapterSessionManager {
801
826
  delete inputs[questionId];
802
827
  }
803
828
  }
829
+ const completedAt = Math.max(this.record.completedAt ?? 0, remote.completedAt ?? 0) || void 0;
804
830
  this.record = {
805
831
  progress: this.dirtyProgress ? this.record.progress : remote.progress,
806
- inputs
832
+ inputs,
833
+ ...completedAt !== void 0 ? { completedAt } : {}
807
834
  };
808
835
  this.revision = stored.revision;
809
836
  }
@@ -827,11 +854,7 @@ export {
827
854
  ChatController,
828
855
  ConsoleErrorAdapter,
829
856
  IdentityInputAdapter,
830
- RuleEngine,
831
857
  SessionManager,
832
- indexedDbAdapter,
833
- localStorageAdapter,
834
- parseSchema,
835
- sessionStorageAdapter
858
+ parseSchema
836
859
  };
837
860
  //# sourceMappingURL=index.mjs.map
@@ -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/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":["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":";AAEO,IAAM,sBAAN,MAAmD;AAAA,EACxD,KAAK,OAAwB;AAC3B,QAAI,OAAO,aAAa,YAAa;AACrC,UAAM,OAAO,MAAM,SAAS,gBAAgB,qBAAqB;AACjE,aAAS,cAAc,IAAI,YAAY,MAAM,EAAE,QAAQ,OAAO,SAAS,KAAK,CAAC,CAAC;AAAA,EAChF;AACF;;;ACRA;AAAA,EACE,uBAAuB;AAAA,OAElB;AAIA,IAAM,sBAAN,cAAkC,wBAAuC;AAAA,EAC9E,YAAY,UAA+B,CAAC,GAAG;AAC7C,UAAM,EAAE,QAAQ,oBAAoB,GAAG,QAAQ,CAAC;AAAA,EAClD;AACF;;;ACTO,IAAM,uBAAN,MAAmD;AAAA,EACxD,UAAU,KAAuB;AAC/B,WAAO;AAAA,EACT;AACF;;;ACNA,OAAO,eAAe;AAOf,IAAM,aAAN,MAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQtB,SAAS,MAAqB,SAA6B;AACzD,QAAI;AAEF,aAAO;AAAA,QACJ,UAAmE,MAAM,MAAM,OAAO;AAAA,MACzF;AAAA,IACF,SAAS,OAAO;AACd,YAAM,EAAE,MAAM,oBAAoB,SAAS,0BAA0B,MAAM;AAAA,IAC7E;AAAA,EACF;AACF;;;ACvBA,IAAM,eAAe,oBAAI,IAAI,CAAC,QAAQ,YAAY,YAAY,SAAS,UAAU,CAAC;AAClF,IAAM,iBAAiB;AAShB,SAAS,YAAY,KAA0B;AACpD,MAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AACzD,UAAM,UAAU,kCAAkC;AAAA,EACpD;AAEA,QAAM,MAAM;AAEZ,MAAI,IAAI,gBAAgB,MAAM,KAAK;AACjC,UAAM,UAAU,4BAA4B;AAAA,EAC9C;AAEA,MAAI,CAAC,MAAM,QAAQ,IAAI,cAAc,CAAC,GAAG;AACvC,UAAM,UAAU,+BAA+B;AAAA,EACjD;AAEA,QAAM,QAAQ,IAAI,cAAc;AAChC,QAAM,QACJ,OAAO,IAAI,cAAc,MAAM,YAAY,IAAI,cAAc,MAAM,OAC/D,IAAI,cAAc,IAClB,CAAC;AAGP,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,cAAc,oBAAI,IAAY;AAEpC,aAAW,QAAQ,OAAO;AACxB,iBAAa,MAAM,SAAS,aAAa,KAAK;AAAA,EAChD;AAGA,aAAW,QAAQ,OAAO;AACxB,UAAM,IAAI;AACV,eAAW,QAAQ,EAAE,WAAW;AAC9B,UAAI,KAAK,mBAAmB,kBAAkB,CAAC,QAAQ,IAAI,KAAK,cAAc,GAAG;AAC/E,cAAM;AAAA,UACJ,6BAA6B,KAAK,cAAc;AAAA,QAClD;AAAA,MACF;AACA,UAAI,KAAK,SAAS,QAAQ,CAAC,YAAY,IAAI,KAAK,YAAY,GAAG;AAC7D,cAAM;AAAA,UACJ,2BAA2B,KAAK,YAAY;AAAA,QAC9C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAUA,SAAS,aACP,KACA,SACA,aACA,OACM;AACN,MAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AACzD,UAAM,UAAU,qCAAqC;AAAA,EACvD;AACA,QAAM,OAAO;AAEb,MAAI,OAAO,KAAK,IAAI,MAAM,YAAY,CAAC,KAAK,IAAI,GAAG;AACjD,UAAM,UAAU,oCAAoC;AAAA,EACtD;AACA,MAAI,QAAQ,IAAI,KAAK,IAAI,CAAW,GAAG;AACrC,UAAM,UAAU,sBAAsB,KAAK,IAAI,CAAC,GAAG;AAAA,EACrD;AACA,UAAQ,IAAI,KAAK,IAAI,CAAW;AAEhC,MAAI,CAAC,MAAM,QAAQ,KAAK,WAAW,CAAC,GAAG;AACrC,UAAM,UAAU,SAAS,KAAK,IAAI,CAAC,+BAA+B;AAAA,EACpE;AAEA,QAAM,YAAY,KAAK,WAAW;AAClC,MAAI,CAAC,MAAM,QAAQ,SAAS,KAAM,UAAwB,WAAW,GAAG;AACtE,UAAM,UAAU,SAAS,KAAK,IAAI,CAAC,yCAAyC;AAAA,EAC9E;AAEA,QAAM,WAAY,UAAwB,UAAU,SAAS,CAAC;AAC9D,MAAI,SAAS,MAAM,MAAM,UAAU;AACjC,UAAM,UAAU,SAAS,KAAK,IAAI,CAAC,mDAAmD;AAAA,EACxF;AAEA,aAAW,KAAK,KAAK,WAAW,GAAgB;AAC9C,qBAAiB,GAAG,KAAK,IAAI,GAAa,aAAa,KAAK;AAAA,EAC9D;AACF;AAUA,SAAS,iBACP,KACA,QACA,aACA,OACM;AACN,MAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AACzD,UAAM,UAAU,yCAAyC;AAAA,EAC3D;AACA,QAAM,IAAI;AAEV,MAAI,OAAO,EAAE,IAAI,MAAM,YAAY,CAAC,EAAE,IAAI,GAAG;AAC3C,UAAM,UAAU,YAAY,MAAM,2CAA2C;AAAA,EAC/E;AACA,MAAI,YAAY,IAAI,EAAE,IAAI,CAAW,GAAG;AACtC,UAAM,UAAU,0BAA0B,EAAE,IAAI,CAAC,GAAG;AAAA,EACtD;AACA,cAAY,IAAI,EAAE,IAAI,CAAW;AAEjC,MAAI,CAAC,aAAa,IAAI,EAAE,cAAc,CAAW,GAAG;AAClD,UAAM;AAAA,MACJ,aAAa,EAAE,IAAI,CAAC,+BAA+B,EAAE,cAAc,CAAC;AAAA,IAEtE;AAAA,EACF;AAEA,QAAM,YAAY,EAAE,kBAAkB;AACtC,MAAI,aAAa,OAAO,cAAc,YAAY,CAAC,MAAM,QAAQ,SAAS,GAAG;AAC3E,UAAM,IAAI;AACV,QAAI,SAAS,KAAK,OAAO,EAAE,KAAK,MAAM,UAAU;AAC9C,UAAI,EAAE,EAAE,KAAK,KAAK,QAAQ;AACxB,cAAM,UAAU,aAAa,EAAE,IAAI,CAAC,yCAAyC,EAAE,KAAK,CAAC,GAAG;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AACF;AAQA,SAAS,UAAU,SAA8D;AAC/E,SAAO,EAAE,MAAM,kBAAkB,QAAQ;AAC3C;;;AChJA,IAAM,cAAc;AACpB,IAAM,cAAc,CAAC,OAAe,gBAAgB,EAAE;AACtD,IAAM,WAAW,CAAC,WAAmB,eAAuB,aAAa,SAAS,IAAI,UAAU;AAChG,IAAM,eAAe,CAAC,cAAsB,iBAAiB,SAAS;AA4B/D,IAAM,iBAAN,MAA6C;AAAA,EAIlD,YAA6B,UAA0B,WAAW,cAAc;AAAnD;AAC3B,SAAK,YAAY,KAAK,aAAa;AACnC,SAAK,YAAY,KAAK,sBAAsB;AAAA,EAC9C;AAAA,EAEA,eAAuB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,qBAA8B;AAChC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,aAAa,eAAuB,SAA0B;AAC5D,QAAI,CAAC,KAAK,UAAW;AACrB,SAAK,QAAQ,QAAQ,YAAY,KAAK,SAAS,GAAG,KAAK,UAAU,EAAE,eAAe,QAAQ,CAAC,CAAC;AAAA,EAC9F;AAAA,EAEA,eAAuC;AACrC,QAAI,CAAC,KAAK,UAAW,QAAO;AAC5B,UAAM,MAAM,KAAK,QAAQ,QAAQ,YAAY,KAAK,SAAS,CAAC;AAC5D,QAAI,CAAC,IAAK,QAAO;AACjB,QAAI;AACF,aAAO,KAAK,MAAM,GAAG;AAAA,IACvB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,UAAU,YAAoB,OAAgC;AAC5D,QAAI,CAAC,KAAK,UAAW;AACrB,SAAK,QAAQ,QAAQ,SAAS,KAAK,WAAW,UAAU,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,EAClF;AAAA,EAEA,UAAU,YAA8C;AACtD,QAAI,CAAC,KAAK,UAAW,QAAO;AAC5B,UAAM,MAAM,KAAK,QAAQ,QAAQ,SAAS,KAAK,WAAW,UAAU,CAAC;AACrE,QAAI,CAAC,IAAK,QAAO;AACjB,QAAI;AACF,aAAO,KAAK,MAAM,GAAG;AAAA,IACvB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,WAAW,YAA0B;AACnC,QAAI,CAAC,KAAK,UAAW;AACrB,SAAK,QAAQ,WAAW,SAAS,KAAK,WAAW,UAAU,CAAC;AAAA,EAC9D;AAAA,EAEA,WAAiB;AACf,SAAK,SAAS;AACd,QAAI,CAAC,KAAK,UAAW;AACrB,SAAK,QAAQ,QAAQ,aAAa,KAAK,SAAS,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC;AAAA,EACvE;AAAA,EAEA,cAA6B;AAC3B,QAAI,CAAC,KAAK,UAAW,QAAO;AAC5B,UAAM,cAAc,OAAO,KAAK,QAAQ,QAAQ,aAAa,KAAK,SAAS,CAAC,CAAC;AAC7E,WAAO,OAAO,SAAS,WAAW,KAAK,cAAc,IAAI,cAAc;AAAA,EACzE;AAAA,EAEA,WAAiB;AACf,QAAI,CAAC,KAAK,UAAW;AACrB,UAAM,cAAc,YAAY,KAAK,SAAS;AAC9C,UAAM,cAAc,aAAa,KAAK,SAAS;AAC/C,UAAM,eAAyB,CAAC,aAAa,aAAa,KAAK,SAAS,CAAC;AAEzE,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,QAAQ,KAAK;AAC5C,YAAM,IAAI,KAAK,QAAQ,IAAI,CAAC;AAC5B,UAAI,GAAG,WAAW,WAAW,EAAG,cAAa,KAAK,CAAC;AAAA,IACrD;AAEA,eAAW,KAAK,cAAc;AAC5B,WAAK,QAAQ,WAAW,CAAC;AAAA,IAC3B;AAAA,EACF;AAAA,EAEQ,wBAAgC;AACtC,QAAI,CAAC,KAAK,UAAW,QAAO,OAAO,WAAW;AAC9C,UAAM,WAAW,KAAK,QAAQ,QAAQ,WAAW;AACjD,QAAI,SAAU,QAAO;AACrB,UAAM,KAAK,OAAO,WAAW;AAC7B,SAAK,QAAQ,QAAQ,aAAa,EAAE;AACpC,WAAO;AAAA,EACT;AAAA,EAEQ,eAAwB;AAC9B,QAAI;AACF,YAAM,QAAQ;AACd,WAAK,QAAQ,QAAQ,OAAO,GAAG;AAC/B,WAAK,QAAQ,WAAW,KAAK;AAC7B,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;AC3HA,IAAM,kBAA+B;AAAA,EACnC,eAAe;AAAA,EACf,cAAc;AAAA,EACd,cAAc;AAAA,EACd,WAAW;AAAA,EACX,mBAAmB;AAAA,EACnB,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,aAAa;AAAA,EACb,eAAe;AAAA,EACf,cAAc;AAAA,EACd,kBAAkB;AACpB;AAGA,SAAS,eAAiC,QAAuB;AAC/D,SAAO,OAAO;AAAA,IACZ,OAAO,QAAQ,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,KAAK,MAAM,UAAU,MAAS;AAAA,EAClE;AACF;AAMO,IAAM,iBAAN,MAAqB;AAAA;AAAA;AAAA;AAAA;AAAA,EAqB1B,YAAY,QAA8B,gBAA+B;AApBzE,SAAQ,SAA4B;AACpC,SAAQ,UAAuB,EAAE,GAAG,gBAAgB;AAGpD,SAAiB,aAAa,IAAI,WAAW;AAC7C,SAAQ,cAA+B;AACvC,SAAQ,aAAwB,CAAC;AACjC,SAAQ,aAAa;AAErB;AAAA,SAAQ,QAAsB,CAAC;AAE/B;AAAA,SAAQ,YAA2B;AAEnC;AAAA,SAAQ,eAA8B;AACtC,SAAiB,YAAY,oBAAI,IAA8B;AAO7D,SAAK,SAAS;AAAA,MACZ,cAAc,OAAO,gBAAgB,IAAI,qBAAqB;AAAA,MAC9D,eAAe,OAAO,iBAAiB,IAAI,oBAAoB;AAAA,MAC/D,cAAc,OAAO,gBAAgB,IAAI,oBAAoB;AAAA,MAC7D,QAAQ,OAAO;AAAA,MACf,SAAS,OAAO,WAAW,CAAC;AAAA,IAC9B;AACA,SAAK,UAAU,kBAAkB,IAAI,eAAe;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAc;AACZ,QAAI;AACJ,QAAI;AACF,YAAM,KAAK,OAAO,aAAa,UAAU,KAAK,OAAO,MAAM;AAAA,IAC7D,SAAS,OAAO;AACd,WAAK,YAAY,EAAE,MAAM,iBAAiB,SAAS,kCAAkC,MAAM,CAAC;AAAA,IAC9F;AAEA,QAAI;AACJ,QAAI;AACF,eAAS,YAAY,GAAG;AAAA,IAC1B,SAAS,GAAG;AACV,WAAK,YAAY,CAAc;AAAA,IACjC;AACA,SAAK,SAAS;AAEd,QAAI,CAAC,KAAK,QAAQ,oBAAoB;AACpC,WAAK,SAAS;AAAA,QACZ,MAAM;AAAA,QACN,SAAS;AAAA,MACX,CAAC;AAAA,IACH;AAIA,UAAM,aAAa,OAAO,gBAAgB,KAAK,CAAC;AAChD,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,MACH,GAAG,eAAe,UAAU;AAAA,MAC5B,GAAG,eAAe,KAAK,OAAO,OAAO;AAAA,IACvC;AAEA,QAAI,KAAK,QAAQ,eAAe;AAC9B,YAAM,QAAQ,KAAK,QAAQ,aAAa;AACxC,UAAI,OAAO;AACT,cAAM,OAAO,OAAO,cAAc,EAAE,KAAK,CAAC,MAAM,EAAE,OAAO,MAAM,aAAa;AAC5E,YAAI,MAAM;AACR,eAAK,aAAa,MAAM;AACxB,eAAK,cAAc;AACnB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,SAAK,cAAc,OAAO,cAAc,EAAE,CAAC;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAkC;AAChC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,sBAAsC;AACpC,QAAI,CAAC,KAAK,YAAa,QAAO,CAAC;AAC/B,WAAO,KAAK,YAAY,UAAU,OAAO,CAAC,MAAM,KAAK,kBAAkB,CAAC,CAAC;AAAA,EAC3E;AAAA;AAAA;AAAA;AAAA,EAKA,aAA0B;AACxB,WAAO,EAAE,GAAG,KAAK,QAAQ;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,WAAyB;AACvB,WAAO,KAAK,MAAM,IAAI,CAAC,WAAW,EAAE,QAAQ,MAAM,QAAQ,SAAS,EAAE,GAAG,MAAM,QAAQ,EAAE,EAAE;AAAA,EAC5F;AAAA;AAAA;AAAA;AAAA,EAKA,aAAwB;AACtB,WAAO,EAAE,GAAG,KAAK,WAAW;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,WAAW,YAA6B;AACtC,QAAI,CAAC,KAAK,UAAU,CAAC,KAAK,YAAa,QAAO;AAC9C,UAAM,QAAQ,KAAK,MAAM,UAAU;AACnC,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,OAAO,KAAK,OAAO,cAAc,EAAE,KAAK,CAAC,MAAM,EAAE,OAAO,MAAM,MAAM;AAC1E,QAAI,CAAC,KAAM,QAAO;AAIlB,QAAI,KAAK,cAAc,KAAM,MAAK,eAAe,KAAK,YAAY;AAClE,SAAK,YAAY;AACjB,SAAK,cAAc;AACnB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,WAAW,aAAuC;AAChD,QAAI,CAAC,KAAK,eAAe,CAAC,KAAK,OAAQ,QAAO;AAE9C,UAAM,SAAS,KAAK,YAAY;AAChC,UAAM,oBAAoB,KAAK;AAC/B,UAAM,cAAc,KAAK;AACzB,UAAM,SAAS,gBAAgB;AAE/B,QAAI,UAAyB;AAC7B,QAAI;AACJ,QAAI,eAA8B;AAElC,QAAI,QAAQ;AACV,YAAM,QAAQ,KAAK,MAAM,WAAW;AAGpC,YAAM,YAAY,EAAE,GAAG,KAAK,WAAW;AACvC,iBAAW,cAAc,OAAO,KAAK,MAAM,OAAO,EAAG,QAAO,UAAU,UAAU;AAChF,WAAK,aAAa,EAAE,GAAG,WAAW,GAAG,YAAY;AACjD,YAAM,UAAU,EAAE,GAAG,YAAY;AAEjC,mBAAa,KAAK,iBAAiB,KAAK,aAAa,KAAK,UAAU;AACpE,YAAM,qBAAqB,KAAK,MAAM,cAAc,CAAC,GAAG,UAAU,KAAK;AAEvE,UAAI,eAAe,oBAAoB;AACrC,kBAAU;AACV,uBAAe,KAAK;AAAA,MACtB,OAAO;AACL,kBAAU;AAEV,mBAAW,SAAS,KAAK,MAAM,MAAM,cAAc,CAAC,GAAG;AACrD,qBAAW,cAAc,OAAO,KAAK,MAAM,OAAO,EAAG,QAAO,KAAK,WAAW,UAAU;AAAA,QACxF;AACA,aAAK,MAAM,SAAS,cAAc;AAAA,MACpC;AACA,WAAK,YAAY;AACjB,WAAK,eAAe;AAAA,IACtB,OAAO;AACL,WAAK,aAAa,EAAE,GAAG,KAAK,YAAY,GAAG,YAAY;AACvD,WAAK,MAAM,KAAK,EAAE,QAAQ,SAAS,EAAE,GAAG,YAAY,EAAE,CAAC;AACvD,mBAAa,KAAK,iBAAiB,KAAK,aAAa,KAAK,UAAU;AAAA,IACtE;AAEA,QAAI,KAAK,QAAQ,eAAe;AAC9B,YAAM,iBACJ,YAAY,kBAAkB,eAC1B,eACA,eAAe,uBACb,SACA;AACR,WAAK,QAAQ,aAAa,gBAAgB,KAAK,UAAU;AAEzD,YAAM,cAAyB;AAAA,QAC7B,MAAM;AAAA,QACN,WAAW,KAAK,QAAQ,aAAa;AAAA,QACrC;AAAA,QACA,YAAY;AAAA,QACZ,SAAS;AAAA,QACT,GAAI,SAAS,EAAE,QAAQ,KAAK,IAAI,CAAC;AAAA,MACnC;AACA,WAAK,SAAS,WAAW;AACzB,WAAK,KAAK,eAAe,WAAW;AAAA,IACtC;AAEA,SAAK;AAEL,QAAI,YAAY,kBAAkB,cAAc;AAC9C,WAAK,cAAc,KAAK,OAAO,cAAc,EAAE,KAAK,CAAC,MAAM,EAAE,OAAO,YAAY,KAAK;AACrF,aAAO;AAAA,IACT;AAEA,QAAI,eAAe,sBAAsB;AACvC,UAAI,KAAK,QAAQ,UAAU;AACzB,aAAK,QAAQ,SAAS;AAAA,MACxB,OAAO;AACL,aAAK,QAAQ,SAAS;AAAA,MACxB;AACA,YAAM,gBAA2B;AAAA,QAC/B,MAAM;AAAA,QACN,WAAW,KAAK,QAAQ,aAAa;AAAA,QACrC,YAAY,KAAK;AAAA,MACnB;AACA,WAAK,SAAS,aAAa;AAC3B,WAAK,KAAK,iBAAiB,aAAa;AACxC,WAAK,cAAc;AAAA,IACrB,OAAO;AACL,WAAK,cAAc,KAAK,OAAO,cAAc,EAAE,KAAK,CAAC,MAAM,EAAE,OAAO,UAAU,KAAK;AAAA,IACrF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,GAAG,OAAkB,UAA0B;AAC7C,QAAI,oBAAoB,KAAK,UAAU,IAAI,KAAK;AAChD,QAAI,CAAC,mBAAmB;AACtB,0BAAoB,oBAAI,IAAI;AAC5B,WAAK,UAAU,IAAI,OAAO,iBAAiB;AAAA,IAC7C;AACA,sBAAkB,IAAI,QAAQ;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,IAAI,OAAkB,UAA0B;AAC9C,SAAK,UAAU,IAAI,KAAK,GAAG,OAAO,QAAQ;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACd,SAAK,QAAQ,UAAU;AACvB,SAAK,UAAU,MAAM;AACrB,SAAK,cAAc;AACnB,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,kBAAkB,UAAiC;AACzD,UAAM,YAAY,SAAS,kBAAkB;AAC7C,QAAI,CAAC,UAAW,QAAO;AAEvB,UAAM,QAAQ,KAAK,SAAS,cAAc,KAAK,CAAC;AAChD,QAAI;AAEJ,QAAI,SAAS,aAAa,OAAQ,UAAgC,QAAQ,UAAU;AAClF,YAAM,MAAO,UAA8B;AAC3C,aAAO,MAAM,GAAG;AAChB,UAAI,CAAC,KAAM,QAAO;AAAA,IACpB,OAAO;AACL,aAAO;AAAA,IACT;AAEA,QAAI;AACF,aAAO,KAAK,WAAW,SAAS,MAAM,KAAK,gBAAgB,CAAC;AAAA,IAC9D,SAAS,GAAG;AACV,WAAK,SAAS,CAAc;AAC5B,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,kBAA6B;AACnC,QAAI,KAAK,cAAc,KAAM,QAAO,KAAK;AACzC,UAAM,OAAO,EAAE,GAAG,KAAK,WAAW;AAClC,eAAW,cAAc,OAAO,KAAK,KAAK,MAAM,KAAK,SAAS,EAAE,OAAO;AACrE,aAAO,KAAK,UAAU;AACxB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,iBAAiB,MAAgB,SAA4B;AACnE,eAAW,QAAQ,KAAK,WAAW;AACjC,UAAI,KAAK,SAAS,SAAU,QAAO,KAAK;AACxC,UAAI,KAAK,SAAS,QAAQ,QAAQ,KAAK,YAAY,MAAM,KAAK,OAAO;AACnE,eAAO,KAAK;AAAA,MACd;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,SAAS,OAAwB;AACvC,QAAI;AACF,WAAK,OAAO,cAAc,KAAK,KAAK;AAAA,IACtC,SAAS,OAAO;AACd,WAAK,SAAS;AAAA,QACZ,MAAM;AAAA,QACN,SAAS,kCAAkC,MAAM,IAAI;AAAA,QACrD;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,KAAK,OAAkB,SAA0B;AACvD,SAAK,UAAU,IAAI,KAAK,GAAG,QAAQ,CAAC,MAAM;AACxC,QAAE,OAAO;AAAA,IACX,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,YAAY,OAAyB;AAC3C,SAAK,SAAS,KAAK;AACnB,UAAM;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,SAAS,OAAwB;AACvC,UAAM,gBAAgB,CAAC,WAA0B;AAC/C,cAAQ,MAAM,+DAA+D,QAAQ,KAAK;AAAA,IAC5F;AACA,QAAI;AAEF,WAAK,QAAQ,QAAQ,KAAK,OAAO,aAAa,IAAI,KAAK,CAAC,EAAE;AAAA,QACxD,CAAC,WAA6C;AAC5C,cAAI,QAAQ,OAAO,OAAO;AACxB,0BAAc,OAAO,KAAK;AAAA,UAC5B;AAAA,QACF;AAAA,QACA;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF;AACF;;;ACrbA,IAAMA,eAAc;AACpB,IAAM,YAAY,CAAC,cAA8B,eAAe,SAAS;AAGzE,IAAM,uBAAuB;AAsB7B,IAAM,cAAc,OAAsB,EAAE,UAAU,MAAM,QAAQ,CAAC,EAAE;AAEvE,SAAS,YAAY,KAAmC;AACtD,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAO;AAAA,MACL,UAAU,OAAO,YAAY;AAAA,MAC7B,QAAQ,OAAO,UAAU,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS,CAAC;AAAA,MAC9E,GAAI,OAAO,OAAO,gBAAgB,WAAW,EAAE,aAAa,OAAO,YAAY,IAAI,CAAC;AAAA,IACtF;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAWO,IAAM,wBAAN,MAAM,uBAA8C;AAAA,EAYjD,YACW,SACA,WACA,WACjB,SACiB,SACjB;AALiB;AACA;AACA;AAEA;AAdnB,SAAiB,cAAc,oBAAI,IAAY;AAC/C,SAAQ,gBAAgB;AAExB;AAAA,SAAQ,eAAe;AACvB,SAAQ,UAAU;AAClB,SAAQ,aAAmC;AAC3C,SAAiB,SAAsB,IAAI,gBAAgB,EAAE;AAC7D,SAAQ,cAAmC;AASzC,UAAM,SAA+B,UAAU,YAAY,QAAQ,KAAK,IAAI;AAC5E,SAAK,SAAS,UAAU,YAAY;AACpC,SAAK,WAAW,SAAS;AACzB,QAAI,WAAW,CAAC,QAAQ;AACtB,WAAK;AAAA,QACH,iBAAiB,SAAS;AAAA,MAC5B;AAAA,IACF;AACA,QAAI,WAAW;AACb,WAAK,cACH,QAAQ,YAAY,UAAU,SAAS,GAAG,CAAC,WAAW,KAAK,YAAY,MAAM,CAAC,KAAK;AAAA,IACvF;AAAA,EACF;AAAA;AAAA,EAGA,aAAa,OACX,SACA,UAAwC,CAAC,GACT;AAChC,UAAM,SAAsB,IAAI,gBAAgB,EAAE;AAClD,UAAM,OAAO,CAAC,YACZ,QAAQ,UAAU,EAAE,MAAM,uBAAuB,QAAQ,CAAC;AAE5D,QAAI,YAA2B;AAC/B,QAAI,YAAY;AAEhB,UAAM,WAA4C,MAAM,QAAQ,IAAIA,cAAa,EAAE,OAAO,CAAC;AAC3F,QAAI,SAAS,IAAI;AACf,kBAAY,SAAS,OAAO,SAAS;AAAA,IACvC,OAAO;AACL,kBAAY;AACZ,WAAK,kCAAkC,SAAS,KAAK,+BAA+B;AAAA,IACtF;AACA,QAAI,aAAa,cAAc,MAAM;AACnC,kBAAY,OAAO,WAAW;AAC9B,YAAM,QAAkC,MAAM,QAAQ,IAAIA,cAAa,WAAW;AAAA,QAChF;AAAA,MACF,CAAC;AACD,UAAI,CAAC,MAAM,IAAI;AACb,oBAAY;AACZ,aAAK,kCAAkC,MAAM,KAAK,+BAA+B;AAAA,MACnF;AAAA,IACF;AACA,UAAM,KAAa,aAAa,OAAO,WAAW;AAElD,QAAI,UAA8B;AAClC,QAAI,WAAW;AACb,YAAM,eAAgD,MAAM,QAAQ,IAAI,UAAU,EAAE,GAAG;AAAA,QACrF;AAAA,MACF,CAAC;AACD,UAAI,aAAa,IAAI;AACnB,kBAAU,aAAa;AAAA,MACzB,OAAO;AACL,oBAAY;AACZ,aAAK,kCAAkC,aAAa,KAAK,+BAA+B;AAAA,MAC1F;AAAA,IACF;AACA,WAAO,IAAI,uBAAsB,SAAS,IAAI,WAAW,SAAS,OAAO;AAAA,EAC3E;AAAA,EAEA,eAAuB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,qBAA8B;AAChC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,aAAa,eAAuB,SAA0B;AAC5D,SAAK,OAAO,WAAW,EAAE,eAAe,QAAQ;AAChD,SAAK,gBAAgB;AACrB,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEA,eAAuC;AACrC,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA,EAEA,UAAU,YAAoB,OAAyB;AACrD,SAAK,OAAO,OAAO,UAAU,IAAI;AACjC,SAAK,YAAY,IAAI,UAAU;AAC/B,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEA,UAAU,YAAuC;AAC/C,WAAO,KAAK,OAAO,OAAO,UAAU,KAAK;AAAA,EAC3C;AAAA,EAEA,WAAW,YAA0B;AACnC,QAAI,EAAE,cAAc,KAAK,OAAO,QAAS;AACzC,WAAO,KAAK,OAAO,OAAO,UAAU;AACpC,SAAK,YAAY,IAAI,UAAU;AAC/B,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEA,WAAiB;AACf,SAAK,SAAS,EAAE,UAAU,MAAM,QAAQ,CAAC,GAAG,aAAa,KAAK,IAAI,EAAE;AACpE,SAAK,YAAY,MAAM;AACvB,SAAK,gBAAgB;AACrB,SAAK,eAAe;AACpB,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEA,cAA6B;AAC3B,WAAO,KAAK,OAAO,eAAe;AAAA,EACpC;AAAA,EAEA,WAAiB;AACf,SAAK,SAAS,YAAY;AAC1B,SAAK,YAAY,MAAM;AACvB,SAAK,gBAAgB;AACrB,SAAK,eAAe;AACpB,SAAK,UAAU;AACf,QAAI,CAAC,KAAK,UAAW;AACrB,SAAK,KAAK,aAAa;AAAA,EACzB;AAAA,EAEA,UAAgB;AACd,SAAK,cAAc;AACnB,SAAK,cAAc;AAAA,EACrB;AAAA;AAAA,EAGA,MAAM,QAAuB;AAC3B,WAAO,KAAK,YAAY;AACtB,YAAM,KAAK;AAAA,IACb;AAAA,EACF;AAAA,EAEQ,OAAO,SAAiB,OAAuB;AACrD,SAAK,QAAQ,UAAU,EAAE,MAAM,uBAAuB,SAAS,MAAM,CAAC;AAAA,EACxE;AAAA,EAEQ,kBAAwB;AAC9B,QAAI,CAAC,KAAK,UAAW;AACrB,SAAK,UAAU;AACf,SAAK,eAAL,KAAK,aAAe,KAAK,MAAM;AAAA,EACjC;AAAA;AAAA,EAGA,MAAc,QAAuB;AACnC,QAAI,YAAY;AAChB,QAAI;AACF,aAAO,KAAK,SAAS;AACnB,aAAK,UAAU;AACf,cAAM,UAA2C,MAAM,KAAK,YAAY;AACxE,oBAAY,YAAY,aAAa,YAAY,IAAI;AACrD,YAAI,YAAY,YAAY;AAC1B,cAAI,aAAa,sBAAsB;AACrC,iBAAK;AAAA,cACH,0BAA0B,KAAK,SAAS,UAAU,SAAS;AAAA,YAC7D;AACA;AAAA,UACF;AACA,eAAK,UAAU;AAAA,QACjB;AAAA,MACF;AAAA,IACF,UAAE;AAEA,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AAAA,EAEA,MAAc,cAAwD;AACpE,UAAM,aAAa,IAAI,IAAY,KAAK,WAAW;AACnD,UAAM,eAAwB,KAAK;AACnC,UAAM,YAAqB,KAAK;AAChC,SAAK,YAAY,MAAM;AACvB,SAAK,gBAAgB;AACrB,SAAK,eAAe;AAEpB,UAAM,SAAmC,MAAM,KAAK,QAAQ;AAAA,MAC1D,UAAU,KAAK,SAAS;AAAA,MACxB,KAAK,UAAU,KAAK,MAAM;AAAA,MAC1B,EAAE,QAAQ,KAAK,QAAQ,YAAY,KAAK,SAAS;AAAA,IACnD;AACA,QAAI,OAAO,IAAI;AACb,WAAK,WAAW,OAAO,MAAM;AAC7B,aAAO;AAAA,IACT;AAGA,eAAW,cAAc,WAAY,MAAK,YAAY,IAAI,UAAU;AACpE,SAAK,gBAAgB,KAAK,iBAAiB;AAC3C,SAAK,eAAe,KAAK,gBAAgB;AAEzC,QAAI,OAAO,SAAS,YAAY;AAC9B,WAAK,OAAO,0BAA0B,KAAK,SAAS,KAAK,OAAO,KAAK,EAAE;AACvE,aAAO;AAAA,IACT;AACA,UAAM,SAA0C,MAAM,KAAK,QAAQ;AAAA,MACjE,UAAU,KAAK,SAAS;AAAA,MACxB,EAAE,QAAQ,KAAK,OAAO;AAAA,IACxB;AACA,QAAI,CAAC,OAAO,IAAI;AACd,WAAK;AAAA,QACH,WAAW,KAAK,SAAS,8DAA8D,OAAO,KAAK;AAAA,MACrG;AACA,aAAO;AAAA,IACT;AACA,SAAK,YAAY,OAAO,KAAK;AAC7B,WAAO;AAAA,EACT;AAAA;AAAA,EAGQ,YAAY,QAAkC;AACpD,UAAM,SAAiB,KAAK,UAAU,KAAK,OAAO,QAAQ;AAC1D,QAAI,CAAC,QAAQ;AACX,UAAI,CAAC,KAAK,iBAAiB,KAAK,YAAY,SAAS,GAAG;AACtD,aAAK,SAAS,YAAY;AAAA,MAC5B;AACA,WAAK,WAAW;AAAA,IAClB,OAAO;AACL,YAAM,SAA+B,YAAY,OAAO,KAAK;AAC7D,UAAI,CAAC,QAAQ;AACX,aAAK;AAAA,UACH,WAAW,KAAK,SAAS;AAAA,QAC3B;AACA,aAAK,WAAW,OAAO;AACvB;AAAA,MACF;AAEA,YAAM,SAAqC,KAAK,eAAe,CAAC,IAAI,EAAE,GAAG,OAAO,OAAO;AACvF,iBAAW,cAAc,KAAK,aAAa;AACzC,cAAM,OAA+B,KAAK,OAAO,OAAO,UAAU;AAElE,YAAI,SAAS,QAAW;AACtB,iBAAO,UAAU,IAAI;AAAA,QACvB,OAAO;AACL,iBAAO,OAAO,UAAU;AAAA,QAC1B;AAAA,MACF;AACA,YAAM,cACJ,KAAK,IAAI,KAAK,OAAO,eAAe,GAAG,OAAO,eAAe,CAAC,KAAK;AACrE,WAAK,SAAS;AAAA,QACZ,UAAU,KAAK,gBAAgB,KAAK,OAAO,WAAW,OAAO;AAAA,QAC7D;AAAA,QACA,GAAI,gBAAgB,SAAY,EAAE,YAAY,IAAI,CAAC;AAAA,MACrD;AACA,WAAK,WAAW,OAAO;AAAA,IACzB;AACA,QAAI,KAAK,UAAU,KAAK,OAAO,QAAQ,MAAM,QAAQ;AACnD,WAAK,QAAQ,mBAAmB,KAAK,OAAO,QAAQ;AAAA,IACtD;AAAA,EACF;AAAA,EAEA,MAAc,eAA8B;AAC1C,UAAM,MAAc,UAAU,KAAK,SAAS;AAC5C,UAAM,SAA+B,KAAK,QAAQ,SAC9C,MAAM,KAAK,QAAQ,OAAO,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,IACtD,MAAM,KAAK,QAAQ,IAAI,KAAK,KAAK,UAAU,YAAY,CAAC,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC;AACtF,QAAI,OAAO,IAAI;AACb,WAAK,WAAW;AAAA,IAClB,OAAO;AACL,WAAK,OAAO,2BAA2B,KAAK,SAAS,KAAK,OAAO,KAAK,EAAE;AAAA,IAC1E;AAAA,EACF;AACF;","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.12.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>",