@richardmcquiston01/chat-and-react-core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +39 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.mjs +39 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -191,6 +191,7 @@ function makeError(message) {
|
|
|
191
191
|
var KEY_SESSION = "car:session-id";
|
|
192
192
|
var keyProgress = (id) => `car:progress:${id}`;
|
|
193
193
|
var keyInput = (sessionId, questionId) => `car:input:${sessionId}:${questionId}`;
|
|
194
|
+
var keyCompleted = (sessionId) => `car:completed:${sessionId}`;
|
|
194
195
|
var SessionManager = class {
|
|
195
196
|
constructor(storage = globalThis.localStorage) {
|
|
196
197
|
this.storage = storage;
|
|
@@ -235,11 +236,21 @@ var SessionManager = class {
|
|
|
235
236
|
if (!this.available) return;
|
|
236
237
|
this.storage.removeItem(keyInput(this.sessionId, questionId));
|
|
237
238
|
}
|
|
239
|
+
complete() {
|
|
240
|
+
this.clearAll();
|
|
241
|
+
if (!this.available) return;
|
|
242
|
+
this.storage.setItem(keyCompleted(this.sessionId), String(Date.now()));
|
|
243
|
+
}
|
|
244
|
+
completedAt() {
|
|
245
|
+
if (!this.available) return null;
|
|
246
|
+
const completedAt = Number(this.storage.getItem(keyCompleted(this.sessionId)));
|
|
247
|
+
return Number.isFinite(completedAt) && completedAt > 0 ? completedAt : null;
|
|
248
|
+
}
|
|
238
249
|
clearAll() {
|
|
239
250
|
if (!this.available) return;
|
|
240
251
|
const progressKey = keyProgress(this.sessionId);
|
|
241
252
|
const inputPrefix = `car:input:${this.sessionId}:`;
|
|
242
|
-
const keysToRemove = [progressKey];
|
|
253
|
+
const keysToRemove = [progressKey, keyCompleted(this.sessionId)];
|
|
243
254
|
for (let i = 0; i < this.storage.length; i++) {
|
|
244
255
|
const k = this.storage.key(i);
|
|
245
256
|
if (k?.startsWith(inputPrefix)) keysToRemove.push(k);
|
|
@@ -479,7 +490,11 @@ var ChatController = class {
|
|
|
479
490
|
return outcome;
|
|
480
491
|
}
|
|
481
492
|
if (nextPageId === "**FORM_COMPLETED**") {
|
|
482
|
-
this.session.
|
|
493
|
+
if (this.session.complete) {
|
|
494
|
+
this.session.complete();
|
|
495
|
+
} else {
|
|
496
|
+
this.session.clearAll();
|
|
497
|
+
}
|
|
483
498
|
const completeEvent = {
|
|
484
499
|
type: "form:complete",
|
|
485
500
|
sessionId: this.session.getSessionId(),
|
|
@@ -637,7 +652,8 @@ function parseRecord(raw) {
|
|
|
637
652
|
const parsed = JSON.parse(raw);
|
|
638
653
|
return {
|
|
639
654
|
progress: parsed.progress ?? null,
|
|
640
|
-
inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {}
|
|
655
|
+
inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {},
|
|
656
|
+
...typeof parsed.completedAt === "number" ? { completedAt: parsed.completedAt } : {}
|
|
641
657
|
};
|
|
642
658
|
} catch {
|
|
643
659
|
return null;
|
|
@@ -651,6 +667,8 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
651
667
|
this.options = options;
|
|
652
668
|
this.dirtyInputs = /* @__PURE__ */ new Set();
|
|
653
669
|
this.dirtyProgress = false;
|
|
670
|
+
/** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
|
|
671
|
+
this.resetPending = false;
|
|
654
672
|
this.pending = false;
|
|
655
673
|
this.persisting = null;
|
|
656
674
|
this.signal = new AbortController().signal;
|
|
@@ -733,10 +751,21 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
733
751
|
this.dirtyInputs.add(questionId);
|
|
734
752
|
this.schedulePersist();
|
|
735
753
|
}
|
|
754
|
+
complete() {
|
|
755
|
+
this.record = { progress: null, inputs: {}, completedAt: Date.now() };
|
|
756
|
+
this.dirtyInputs.clear();
|
|
757
|
+
this.dirtyProgress = true;
|
|
758
|
+
this.resetPending = true;
|
|
759
|
+
this.schedulePersist();
|
|
760
|
+
}
|
|
761
|
+
completedAt() {
|
|
762
|
+
return this.record.completedAt ?? null;
|
|
763
|
+
}
|
|
736
764
|
clearAll() {
|
|
737
765
|
this.record = emptyRecord();
|
|
738
766
|
this.dirtyInputs.clear();
|
|
739
767
|
this.dirtyProgress = false;
|
|
768
|
+
this.resetPending = false;
|
|
740
769
|
this.pending = false;
|
|
741
770
|
if (!this.available) return;
|
|
742
771
|
void this.removeRecord();
|
|
@@ -784,8 +813,10 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
784
813
|
async persistOnce() {
|
|
785
814
|
const sentInputs = new Set(this.dirtyInputs);
|
|
786
815
|
const sentProgress = this.dirtyProgress;
|
|
816
|
+
const sentReset = this.resetPending;
|
|
787
817
|
this.dirtyInputs.clear();
|
|
788
818
|
this.dirtyProgress = false;
|
|
819
|
+
this.resetPending = false;
|
|
789
820
|
const result = await this.adapter.set(
|
|
790
821
|
keyRecord(this.sessionId),
|
|
791
822
|
JSON.stringify(this.record),
|
|
@@ -797,6 +828,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
797
828
|
}
|
|
798
829
|
for (const questionId of sentInputs) this.dirtyInputs.add(questionId);
|
|
799
830
|
this.dirtyProgress = this.dirtyProgress || sentProgress;
|
|
831
|
+
this.resetPending = this.resetPending || sentReset;
|
|
800
832
|
if (result.code !== "conflict") {
|
|
801
833
|
this.report(`Could not save session ${this.sessionId}: ${result.error}`);
|
|
802
834
|
return "failed";
|
|
@@ -831,7 +863,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
831
863
|
this.revision = stored.revision;
|
|
832
864
|
return;
|
|
833
865
|
}
|
|
834
|
-
const inputs = { ...remote.inputs };
|
|
866
|
+
const inputs = this.resetPending ? {} : { ...remote.inputs };
|
|
835
867
|
for (const questionId of this.dirtyInputs) {
|
|
836
868
|
const mine = this.record.inputs[questionId];
|
|
837
869
|
if (mine !== void 0) {
|
|
@@ -840,9 +872,11 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
840
872
|
delete inputs[questionId];
|
|
841
873
|
}
|
|
842
874
|
}
|
|
875
|
+
const completedAt = Math.max(this.record.completedAt ?? 0, remote.completedAt ?? 0) || void 0;
|
|
843
876
|
this.record = {
|
|
844
877
|
progress: this.dirtyProgress ? this.record.progress : remote.progress,
|
|
845
|
-
inputs
|
|
878
|
+
inputs,
|
|
879
|
+
...completedAt !== void 0 ? { completedAt } : {}
|
|
846
880
|
};
|
|
847
881
|
this.revision = stored.revision;
|
|
848
882
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBA,qBAGO;AAEP,uBAAiC;;;ACrB1B,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,+BAGO;AAIA,IAAM,sBAAN,cAAkC,yBAAAA,oBAAuC;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,2BAAsB;AAOf,IAAM,aAAN,MAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQtB,SAAS,MAAqB,SAA6B;AACzD,QAAI;AAEF,aAAO;AAAA,QACJ,qBAAAC,QAAmE,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;AAoBzF,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,QAAI,CAAC,KAAK,UAAW;AACrB,UAAM,cAAc,YAAY,KAAK,SAAS;AAC9C,UAAM,cAAc,aAAa,KAAK,SAAS;AAC/C,UAAM,eAAyB,CAAC,WAAW;AAE3C,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;;;ACtGA,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,WAAK,QAAQ,SAAS;AACtB,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;;;ACjbA,IAAMC,eAAc;AACpB,IAAM,YAAY,CAAC,cAA8B,eAAe,SAAS;AAGzE,IAAM,uBAAuB;AAoB7B,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,IAChF;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAWO,IAAM,wBAAN,MAAM,uBAA8C;AAAA,EAUjD,YACW,SACA,WACA,WACjB,SACiB,SACjB;AALiB;AACA;AACA;AAEA;AAZnB,SAAiB,cAAc,oBAAI,IAAY;AAC/C,SAAQ,gBAAgB;AACxB,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,YAAY;AAC1B,SAAK,YAAY,MAAM;AACvB,SAAK,gBAAgB;AACrB,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,SAAK,YAAY,MAAM;AACvB,SAAK,gBAAgB;AAErB,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;AAE3C,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;AACA,YAAM,SAAqC,EAAE,GAAG,OAAO,OAAO;AAC9D,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,WAAK,SAAS;AAAA,QACZ,UAAU,KAAK,gBAAgB,KAAK,OAAO,WAAW,OAAO;AAAA,QAC7D;AAAA,MACF;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":["BaseConsoleErrorAdapter","jsonLogic","KEY_SESSION"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/adapters/browser-event-adapter.ts","../src/adapters/console-error-adapter.ts","../src/adapters/identity-input-adapter.ts","../src/rule-engine.ts","../src/schema-parser.ts","../src/session-manager.ts","../src/chat-controller.ts","../src/storage/adapter-session-manager.ts"],"sourcesContent":["export type {\n StorageAdapter,\n StoredValue,\n StoreErrorCode,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\n// The browser storage adapters live in the storage-adapters package; re-exported here so\n// they can be imported from core. Server-side adapters (SQLite, files) are in the adapters\n// package.\nexport type {\n LocalStorageAdapterOptions,\n SessionStorageAdapterOptions,\n StorageChangeEvent,\n StorageEventTarget,\n WebStorageAdapterOptions,\n WebStorageLike,\n WebStorageName,\n} from '@richardmcquiston01/storage-adapters/browser';\nexport {\n localStorageAdapter,\n sessionStorageAdapter,\n} from '@richardmcquiston01/storage-adapters/browser';\nexport type { IndexedDbAdapterOptions } from '@richardmcquiston01/storage-adapters/indexeddb';\nexport { indexedDbAdapter } from '@richardmcquiston01/storage-adapters/indexeddb';\nexport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nexport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nexport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nexport { ChatController } from './chat-controller.js';\nexport { RuleEngine } from './rule-engine.js';\nexport { parseSchema } from './schema-parser.js';\nexport type { SessionProgress, SessionStore, StorageBackend } from './session-manager.js';\nexport { SessionManager } from './session-manager.js';\nexport type { AdapterSessionManagerOptions } from './storage/adapter-session-manager.js';\nexport { AdapterSessionManager } from './storage/adapter-session-manager.js';\nexport * from './types.js';\n","import type { ChatEvent, OutputAdapter } from '../types.js';\n\nexport class BrowserEventAdapter implements OutputAdapter {\n emit(event: ChatEvent): void {\n if (typeof document === 'undefined') return;\n const name = event.type === 'page:submit' ? 'chat-page-submit' : 'chat-form-complete';\n document.dispatchEvent(new CustomEvent(name, { detail: event, bubbles: true }));\n }\n}\n","import {\n ConsoleErrorAdapter as BaseConsoleErrorAdapter,\n type ConsoleErrorOptions,\n} from '@richardmcquiston01/error-log-adapter';\nimport type { ChatErrorCode } from '../types.js';\n\n/** Writes errors to `console.error`, prefixed with `[chat-and-react]` unless overridden. */\nexport class ConsoleErrorAdapter extends BaseConsoleErrorAdapter<ChatErrorCode> {\n constructor(options: ConsoleErrorOptions = {}) {\n super({ prefix: '[chat-and-react]', ...options });\n }\n}\n","import type { InputAdapter } from '../types.js';\n\nexport class IdentityInputAdapter implements InputAdapter {\n transform(raw: unknown): unknown {\n return raw;\n }\n}\n","import jsonLogic from 'json-logic-js';\nimport type { AnswerMap, JsonLogicRule } from './types.js';\n\n/**\n * Evaluates JSON Logic rules against provided answers.\n * Wraps json-logic-js to provide type-safe rule evaluation.\n */\nexport class RuleEngine {\n /**\n * Evaluates a JSON Logic rule against an answer map.\n * @param rule The JSON Logic rule to evaluate\n * @param answers The answer map to evaluate the rule against\n * @returns The boolean result of the rule evaluation\n * @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails\n */\n evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean {\n try {\n // json-logic-js apply() returns any; cast to boolean\n return Boolean(\n (jsonLogic as { apply: (rule: unknown, data: unknown) => unknown }).apply(rule, answers),\n );\n } catch (cause) {\n throw { code: 'RULE_EVAL_FAILED', message: 'Rule evaluation failed', cause };\n }\n }\n}\n","import type { ChatPage, ChatSchema } from './types.js';\n\nconst VALID_INPUTS = new Set(['text', 'textarea', 'dropdown', 'radio', 'checkbox']);\nconst FORM_COMPLETED = '**FORM_COMPLETED**';\n\n/**\n * Parses and validates an unknown input as a {@link ChatSchema}.\n *\n * @param raw - The raw input to validate.\n * @returns The typed {@link ChatSchema} if valid.\n * @throws `{ code: 'SCHEMA_INVALID', message: string }` on any validation failure.\n */\nexport function parseSchema(raw: unknown): ChatSchema {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Schema must be a non-null object');\n }\n\n const obj = raw as Record<string, unknown>;\n\n if (obj['x-chat-version'] !== '1') {\n throw makeError('x-chat-version must be \"1\"');\n }\n\n if (!Array.isArray(obj['x-chat-pages'])) {\n throw makeError('x-chat-pages must be an array');\n }\n\n const pages = obj['x-chat-pages'] as unknown[];\n const rules = (\n typeof obj['x-chat-rules'] === 'object' && obj['x-chat-rules'] !== null\n ? obj['x-chat-rules']\n : {}\n ) as Record<string, unknown>;\n\n const pageIds = new Set<string>();\n const questionIds = new Set<string>();\n\n for (const page of pages) {\n validatePage(page, pageIds, questionIds, rules);\n }\n\n // Second pass: validate branching destinations now that all page ids are known\n for (const page of pages) {\n const p = page as ChatPage;\n for (const rule of p.branching) {\n if (rule.destination_id !== FORM_COMPLETED && !pageIds.has(rule.destination_id)) {\n throw makeError(\n `branching destination_id \"${rule.destination_id}\" does not reference a known page id`,\n );\n }\n if (rule.type === 'if' && !questionIds.has(rule.form_element)) {\n throw makeError(\n `branching form_element \"${rule.form_element}\" does not reference a known question id`,\n );\n }\n }\n }\n\n return raw as ChatSchema;\n}\n\n/**\n * Validates a single page object, collecting its id and question ids.\n *\n * @param raw - The raw page value.\n * @param pageIds - Set of already-seen page ids (mutated in place).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validatePage(\n raw: unknown,\n pageIds: Set<string>,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each page must be a non-null object');\n }\n const page = raw as Record<string, unknown>;\n\n if (typeof page['id'] !== 'string' || !page['id']) {\n throw makeError('Page id must be a non-empty string');\n }\n if (pageIds.has(page['id'] as string)) {\n throw makeError(`Duplicate page id \"${page['id']}\"`);\n }\n pageIds.add(page['id'] as string);\n\n if (!Array.isArray(page['questions'])) {\n throw makeError(`Page \"${page['id']}\" must have a questions array`);\n }\n\n const branching = page['branching'];\n if (!Array.isArray(branching) || (branching as unknown[]).length === 0) {\n throw makeError(`Page \"${page['id']}\" must have a non-empty branching array`);\n }\n\n const lastRule = (branching as unknown[])[branching.length - 1] as Record<string, unknown>;\n if (lastRule['type'] !== 'always') {\n throw makeError(`Page \"${page['id']}\" branching must end with a rule of type \"always\"`);\n }\n\n for (const q of page['questions'] as unknown[]) {\n validateQuestion(q, page['id'] as string, questionIds, rules);\n }\n}\n\n/**\n * Validates a single question object, collecting its id.\n *\n * @param raw - The raw question value.\n * @param pageId - The id of the containing page (for error messages).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validateQuestion(\n raw: unknown,\n pageId: string,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each question must be a non-null object');\n }\n const q = raw as Record<string, unknown>;\n\n if (typeof q['id'] !== 'string' || !q['id']) {\n throw makeError(`In page \"${pageId}\": question id must be a non-empty string`);\n }\n if (questionIds.has(q['id'] as string)) {\n throw makeError(`Duplicate question id \"${q['id']}\"`);\n }\n questionIds.add(q['id'] as string);\n\n if (!VALID_INPUTS.has(q['x-chat-input'] as string)) {\n throw makeError(\n `Question \"${q['id']}\" has invalid x-chat-input \"${q['x-chat-input']}\". ` +\n `Must be one of: text, textarea, dropdown, radio, checkbox`,\n );\n }\n\n const condition = q['x-chat-condition'];\n if (condition && typeof condition === 'object' && !Array.isArray(condition)) {\n const c = condition as Record<string, unknown>;\n if ('ref' in c && typeof c['ref'] === 'string') {\n if (!(c['ref'] in rules)) {\n throw makeError(`Question \"${q['id']}\" x-chat-condition refs unknown rule \"${c['ref']}\"`);\n }\n }\n }\n}\n\n/**\n * Creates a schema validation error object.\n *\n * @param message - Human-readable description of the validation failure.\n * @returns An error object with `code: 'SCHEMA_INVALID'`.\n */\nfunction makeError(message: string): { code: 'SCHEMA_INVALID'; message: string } {\n return { code: 'SCHEMA_INVALID', message };\n}\n","// packages/core/src/session-manager.ts\nimport type { AnswerMap } from './types.js';\n\nexport interface StorageBackend {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n key(index: number): string | null;\n readonly length: number;\n}\n\nexport interface SessionProgress {\n currentPageId: string;\n answers: AnswerMap;\n}\n\nconst KEY_SESSION = 'car:session-id';\nconst keyProgress = (id: string) => `car:progress:${id}`;\nconst keyInput = (sessionId: string, questionId: string) => `car:input:${sessionId}:${questionId}`;\nconst keyCompleted = (sessionId: string) => `car:completed:${sessionId}`;\n\n/**\n * What `ChatController` needs from session storage. `SessionManager` keeps it in a\n * synchronous Web Storage object; `AdapterSessionManager` keeps it in a `StorageAdapter`.\n */\nexport interface SessionStore {\n readonly isStorageAvailable: boolean;\n getSessionId(): string;\n saveProgress(currentPageId: string, answers: AnswerMap): void;\n loadProgress(): SessionProgress | null;\n saveInput(questionId: string, value: string | string[]): void;\n loadInput(questionId: string): string | string[] | null;\n /** Forgets a saved input, such as a draft that has been submitted. */\n clearInput(questionId: string): void;\n clearAll(): void;\n /**\n * Called when the form is finished. Clears the saved session like `clearAll()` and also records\n * that it was completed, so another tab that is still open can tell \"finished\" from \"cleared\".\n * A store without it is cleared with `clearAll()`.\n */\n complete?(): void;\n /** When `complete()` was last called, in any tab (milliseconds since the epoch), or null. */\n completedAt?(): number | null;\n /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */\n dispose?(): void;\n}\n\nexport class SessionManager implements SessionStore {\n private readonly sessionId: string;\n private readonly available: boolean;\n\n constructor(private readonly storage: StorageBackend = globalThis.localStorage) {\n this.available = this.checkStorage();\n this.sessionId = this.loadOrCreateSessionId();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n if (!this.available) return;\n this.storage.setItem(keyProgress(this.sessionId), JSON.stringify({ currentPageId, answers }));\n }\n\n loadProgress(): SessionProgress | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyProgress(this.sessionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as SessionProgress;\n } catch {\n return null;\n }\n }\n\n saveInput(questionId: string, value: string | string[]): void {\n if (!this.available) return;\n this.storage.setItem(keyInput(this.sessionId, questionId), JSON.stringify(value));\n }\n\n loadInput(questionId: string): string | string[] | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyInput(this.sessionId, questionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as string | string[];\n } catch {\n return null;\n }\n }\n\n clearInput(questionId: string): void {\n if (!this.available) return;\n this.storage.removeItem(keyInput(this.sessionId, questionId));\n }\n\n complete(): void {\n this.clearAll();\n if (!this.available) return;\n this.storage.setItem(keyCompleted(this.sessionId), String(Date.now()));\n }\n\n completedAt(): number | null {\n if (!this.available) return null;\n const completedAt = Number(this.storage.getItem(keyCompleted(this.sessionId)));\n return Number.isFinite(completedAt) && completedAt > 0 ? completedAt : null;\n }\n\n clearAll(): void {\n if (!this.available) return;\n const progressKey = keyProgress(this.sessionId);\n const inputPrefix = `car:input:${this.sessionId}:`;\n const keysToRemove: string[] = [progressKey, keyCompleted(this.sessionId)];\n\n for (let i = 0; i < this.storage.length; i++) {\n const k = this.storage.key(i);\n if (k?.startsWith(inputPrefix)) keysToRemove.push(k);\n }\n\n for (const k of keysToRemove) {\n this.storage.removeItem(k);\n }\n }\n\n private loadOrCreateSessionId(): string {\n if (!this.available) return crypto.randomUUID();\n const existing = this.storage.getItem(KEY_SESSION);\n if (existing) return existing;\n const id = crypto.randomUUID();\n this.storage.setItem(KEY_SESSION, id);\n return id;\n }\n\n private checkStorage(): boolean {\n try {\n const probe = '__car_probe__';\n this.storage.setItem(probe, '1');\n this.storage.removeItem(probe);\n return true;\n } catch {\n return false;\n }\n }\n}\n","// packages/core/src/chat-controller.ts\n\nimport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nimport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nimport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nimport { RuleEngine } from './rule-engine.js';\nimport { parseSchema } from './schema-parser.js';\nimport { SessionManager, type SessionStore } from './session-manager.js';\nimport type {\n AnswerMap,\n ChatControllerConfig,\n ChatError,\n ChatEvent,\n ChatOptions,\n ChatPage,\n ChatQuestion,\n ChatSchema,\n ErrorLogResult,\n RouteEntry,\n SubmitOutcome,\n} from './types.js';\n\ntype EventName = 'page:submit' | 'form:complete';\ntype Listener = (event: ChatEvent) => void;\n\nconst DEFAULT_OPTIONS: ChatOptions = {\n userResumable: true,\n localStorage: true,\n autoComplete: true,\n allowEdit: false,\n autoHideQuestions: 0,\n autoHideAnswers: 0,\n hideAnimation: 'fade',\n typingSpeed: 0,\n thinkingDelay: 0,\n thinkingText: '',\n showContinueIcon: true,\n};\n\n/** Copies an object without its undefined values, so they cannot hide a default. */\nfunction stripUndefined<T extends object>(source: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(source).filter(([, value]) => value !== undefined),\n ) as Partial<T>;\n}\n\n/**\n * Central orchestrator for the chat-and-react form flow.\n * Drives page navigation, branching, session persistence, and event emission.\n */\nexport class ChatController {\n private schema: ChatSchema | null = null;\n private options: ChatOptions = { ...DEFAULT_OPTIONS };\n private readonly config: Required<ChatControllerConfig>;\n private readonly session: SessionStore;\n private readonly ruleEngine = new RuleEngine();\n private currentPage: ChatPage | null = null;\n private allAnswers: AnswerMap = {};\n private visitIndex = 0;\n /** Pages submitted in this session, in visit order. Used for editing earlier answers. */\n private route: RouteEntry[] = [];\n /** Index in `route` of the page being edited, or null when not editing. */\n private editIndex: number | null = null;\n /** The page the user was on before they started editing an earlier one. */\n private resumePageId: string | null = null;\n private readonly listeners = new Map<EventName, Set<Listener>>();\n\n /**\n * @param config - Controller configuration including schema and optional adapters.\n * @param sessionManager - Optional session manager for test injection.\n */\n constructor(config: ChatControllerConfig, sessionManager?: SessionStore) {\n this.config = {\n inputAdapter: config.inputAdapter ?? new IdentityInputAdapter(),\n outputAdapter: config.outputAdapter ?? new BrowserEventAdapter(),\n errorAdapter: config.errorAdapter ?? new ConsoleErrorAdapter(),\n schema: config.schema,\n options: config.options ?? {},\n };\n this.session = sessionManager ?? new SessionManager();\n }\n\n /**\n * Initialises the controller: parses the schema, resolves options,\n * restores any saved session, and positions to the first page.\n */\n start(): void {\n let raw: unknown;\n try {\n raw = this.config.inputAdapter.transform(this.config.schema);\n } catch (cause) {\n this.handleFatal({ code: 'ADAPTER_ERROR', message: 'InputAdapter.transform() threw', cause });\n }\n\n let schema: ChatSchema;\n try {\n schema = parseSchema(raw);\n } catch (e) {\n this.handleFatal(e as ChatError);\n }\n this.schema = schema;\n\n if (!this.session.isStorageAvailable) {\n this.logError({\n code: 'STORAGE_UNAVAILABLE',\n message: 'localStorage unavailable; running in memory-only mode',\n });\n }\n\n // Defaults, then the schema, then overrides passed in by the host (for\n // example element attributes).\n const schemaOpts = schema['x-chat-options'] ?? {};\n this.options = {\n ...DEFAULT_OPTIONS,\n ...stripUndefined(schemaOpts),\n ...stripUndefined(this.config.options),\n };\n\n if (this.options.userResumable) {\n const saved = this.session.loadProgress();\n if (saved) {\n const page = schema['x-chat-pages'].find((p) => p.id === saved.currentPageId);\n if (page) {\n this.allAnswers = saved.answers;\n this.currentPage = page;\n return;\n }\n }\n }\n\n this.currentPage = schema['x-chat-pages'][0];\n }\n\n /**\n * Returns the page currently being presented to the user, or null if complete.\n */\n getCurrentPage(): ChatPage | null {\n return this.currentPage;\n }\n\n /**\n * Returns the subset of questions on the current page that pass their\n * `x-chat-condition` rule (if any). Questions without a condition are always visible.\n */\n getVisibleQuestions(): ChatQuestion[] {\n if (!this.currentPage) return [];\n return this.currentPage.questions.filter((q) => this.isQuestionVisible(q));\n }\n\n /**\n * Returns the resolved options for the current session (schema options merged with defaults).\n */\n getOptions(): ChatOptions {\n return { ...this.options };\n }\n\n /**\n * Returns the pages the user has submitted in this session, in visit order.\n * A session restored from storage starts with an empty route.\n */\n getRoute(): RouteEntry[] {\n return this.route.map((entry) => ({ pageId: entry.pageId, answers: { ...entry.answers } }));\n }\n\n /**\n * Returns every answer given so far, for pre-filling inputs.\n */\n getAnswers(): AnswerMap {\n return { ...this.allAnswers };\n }\n\n /**\n * Re-opens a page the user already submitted so its answers can be changed.\n * Call `submitPage` afterwards to save the new answers. Not available once the\n * form is complete.\n *\n * @param routeIndex - Position of the page in `getRoute()`.\n * @returns true when the page was re-opened.\n */\n reopenPage(routeIndex: number): boolean {\n if (!this.schema || !this.currentPage) return false;\n const entry = this.route[routeIndex];\n if (!entry) return false;\n const page = this.schema['x-chat-pages'].find((p) => p.id === entry.pageId);\n if (!page) return false;\n\n // Remember where the user was, but only the first time: editing another\n // earlier page before finishing this edit must still return to that page.\n if (this.editIndex === null) this.resumePageId = this.currentPage.id;\n this.editIndex = routeIndex;\n this.currentPage = page;\n return true;\n }\n\n /**\n * Submits the answers for the current page and advances the session.\n * - Accumulates answers into `allAnswers`.\n * - Emits `page:submit` (suppressed when `userResumable` is false).\n * - Resolves branching to find the next page.\n * - Emits `form:complete` and clears session when `**FORM_COMPLETED**` is reached.\n *\n * When the page was re-opened with `reopenPage`, the new answers replace the\n * old ones and `page:submit` is emitted with `edited: true`. If the page's\n * branching still leads to the same next page, the form returns to the page the\n * user was on. If it leads somewhere else, the pages that followed it are\n * dropped, along with their answers, and the form continues from the new page.\n *\n * @param pageAnswers - Answers collected for the current page.\n */\n submitPage(pageAnswers: AnswerMap): SubmitOutcome {\n if (!this.currentPage || !this.schema) return 'advanced';\n\n const pageId = this.currentPage.id;\n const currentVisitIndex = this.visitIndex;\n const editedIndex = this.editIndex;\n const isEdit = editedIndex !== null;\n\n let outcome: SubmitOutcome = 'advanced';\n let nextPageId: string;\n let resumePageId: string | null = null;\n\n if (isEdit) {\n const entry = this.route[editedIndex];\n // Replace the old answers for this page, including any question that is\n // no longer visible after the change.\n const remaining = { ...this.allAnswers };\n for (const questionId of Object.keys(entry.answers)) delete remaining[questionId];\n this.allAnswers = { ...remaining, ...pageAnswers };\n entry.answers = { ...pageAnswers };\n\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n const expectedNextPageId = this.route[editedIndex + 1]?.pageId ?? this.resumePageId;\n\n if (nextPageId === expectedNextPageId) {\n outcome = 'edit-resumed';\n resumePageId = this.resumePageId;\n } else {\n outcome = 'edit-rerouted';\n // The route changed: forget the pages that followed and their answers.\n for (const later of this.route.slice(editedIndex + 1)) {\n for (const questionId of Object.keys(later.answers)) delete this.allAnswers[questionId];\n }\n this.route.length = editedIndex + 1;\n }\n this.editIndex = null;\n this.resumePageId = null;\n } else {\n this.allAnswers = { ...this.allAnswers, ...pageAnswers };\n this.route.push({ pageId, answers: { ...pageAnswers } });\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n }\n\n if (this.options.userResumable) {\n const progressPageId =\n outcome === 'edit-resumed' && resumePageId\n ? resumePageId\n : nextPageId === '**FORM_COMPLETED**'\n ? pageId\n : nextPageId;\n this.session.saveProgress(progressPageId, this.allAnswers);\n\n const submitEvent: ChatEvent = {\n type: 'page:submit',\n sessionId: this.session.getSessionId(),\n pageId,\n visitIndex: currentVisitIndex,\n answers: pageAnswers,\n ...(isEdit ? { edited: true } : {}),\n };\n this.safeEmit(submitEvent);\n this.emit('page:submit', submitEvent);\n }\n\n this.visitIndex++;\n\n if (outcome === 'edit-resumed' && resumePageId) {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === resumePageId) ?? null;\n return outcome;\n }\n\n if (nextPageId === '**FORM_COMPLETED**') {\n if (this.session.complete) {\n this.session.complete();\n } else {\n this.session.clearAll();\n }\n const completeEvent: ChatEvent = {\n type: 'form:complete',\n sessionId: this.session.getSessionId(),\n allAnswers: this.allAnswers,\n };\n this.safeEmit(completeEvent);\n this.emit('form:complete', completeEvent);\n this.currentPage = null;\n } else {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === nextPageId) ?? null;\n }\n return outcome;\n }\n\n /**\n * Registers an event listener for the given event type.\n *\n * @param event - The event to listen for.\n * @param listener - The callback to invoke when the event fires.\n */\n on(event: EventName, listener: Listener): void {\n let listenersForEvent = this.listeners.get(event);\n if (!listenersForEvent) {\n listenersForEvent = new Set();\n this.listeners.set(event, listenersForEvent);\n }\n listenersForEvent.add(listener);\n }\n\n /**\n * Removes a previously registered event listener.\n *\n * @param event - The event type to deregister from.\n * @param listener - The specific listener to remove.\n */\n off(event: EventName, listener: Listener): void {\n this.listeners.get(event)?.delete(listener);\n }\n\n /**\n * Tears down the controller: removes all listeners and nulls internal state.\n */\n destroy(): void {\n this.session.dispose?.();\n this.listeners.clear();\n this.currentPage = null;\n this.schema = null;\n }\n\n /**\n * Evaluates whether a question should be shown based on its `x-chat-condition`.\n * Returns true when no condition is present. On evaluation failure, logs and returns false.\n */\n private isQuestionVisible(question: ChatQuestion): boolean {\n const condition = question['x-chat-condition'];\n if (!condition) return true;\n\n const rules = this.schema?.['x-chat-rules'] ?? {};\n let rule: Record<string, unknown>;\n\n if ('ref' in condition && typeof (condition as { ref?: unknown }).ref === 'string') {\n const ref = (condition as { ref: string }).ref;\n rule = rules[ref];\n if (!rule) return false;\n } else {\n rule = condition as Record<string, unknown>;\n }\n\n try {\n return this.ruleEngine.evaluate(rule, this.answersForRules());\n } catch (e) {\n this.logError(e as ChatError);\n return false;\n }\n }\n\n /**\n * The answers question conditions are checked against. While an earlier page is\n * being edited, that page's own answers are left out, so its questions are\n * shown exactly as they were the first time the page was asked.\n */\n private answersForRules(): AnswerMap {\n if (this.editIndex === null) return this.allAnswers;\n const rest = { ...this.allAnswers };\n for (const questionId of Object.keys(this.route[this.editIndex].answers))\n delete rest[questionId];\n return rest;\n }\n\n /**\n * Walks the branching rules top-to-bottom. Returns the first match's destination_id.\n * Falls back to `**FORM_COMPLETED**` if nothing matches (should not happen with valid schemas).\n */\n private resolveBranching(page: ChatPage, answers: AnswerMap): string {\n for (const rule of page.branching) {\n if (rule.type === 'always') return rule.destination_id;\n if (rule.type === 'if' && answers[rule.form_element] === rule.value) {\n return rule.destination_id;\n }\n }\n return '**FORM_COMPLETED**';\n }\n\n /**\n * Calls `outputAdapter.emit()` and swallows adapter errors (logging them instead).\n */\n private safeEmit(event: ChatEvent): void {\n try {\n this.config.outputAdapter.emit(event);\n } catch (cause) {\n this.logError({\n code: 'ADAPTER_ERROR',\n message: `OutputAdapter.emit() threw for ${event.type}`,\n cause,\n });\n }\n }\n\n /**\n * Notifies all registered listeners for the given event.\n */\n private emit(event: EventName, payload: ChatEvent): void {\n this.listeners.get(event)?.forEach((l) => {\n l(payload);\n });\n }\n\n /**\n * Logs the error via the error adapter and then throws it.\n * Used for unrecoverable errors (schema invalid, adapter failure on start).\n */\n private handleFatal(error: ChatError): never {\n this.logError(error);\n throw error;\n }\n\n /**\n * Sends the error to the error adapter without waiting for it. If the adapter\n * reports a failure, rejects, or throws, that is written to `console.error` so\n * the original error is never lost and the form keeps running.\n */\n private logError(entry: ChatError): void {\n const reportFailure = (reason: unknown): void => {\n console.error('[chat-and-react] ErrorLogAdapter failed to record an error:', reason, entry);\n };\n try {\n // `Promise.resolve` also tolerates older custom adapters that return nothing.\n void Promise.resolve(this.config.errorAdapter.log(entry)).then(\n (result: ErrorLogResult | undefined): void => {\n if (result?.ok === false) {\n reportFailure(result.error);\n }\n },\n reportFailure,\n );\n } catch (cause) {\n reportFailure(cause);\n }\n }\n}\n","import type {\n StorageAdapter,\n StoredValue,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\nimport type { SessionProgress, SessionStore } from '../session-manager.js';\nimport type { AnswerMap, ChatError } from '../types.js';\n\nconst KEY_SESSION = 'car:session-id';\nconst keyRecord = (sessionId: string): string => `car:session:${sessionId}`;\n\n/** After this many revision conflicts in a row, a save is abandoned and reported. */\nconst MAX_CONFLICT_RETRIES = 3;\n\ntype InputValue = string | string[];\n\n/** Everything saved for one session, stored under a single key. */\ninterface SessionRecord {\n progress: SessionProgress | null;\n inputs: Record<string, InputValue>;\n /** When the form was finished (milliseconds since the epoch), if it has been. */\n completedAt?: number;\n}\n\nexport interface AdapterSessionManagerOptions {\n /** Called when the adapter fails. The session keeps working from memory. */\n onError?: (error: ChatError) => void;\n /**\n * Called when saved progress changes because of a write made elsewhere, such as\n * another tab. `null` means the session was cleared there.\n */\n onExternalChange?: (progress: SessionProgress | null) => void;\n}\n\nconst emptyRecord = (): SessionRecord => ({ progress: null, inputs: {} });\n\nfunction parseRecord(raw: string): SessionRecord | null {\n try {\n const parsed = JSON.parse(raw) as Partial<SessionRecord>;\n return {\n progress: parsed.progress ?? null,\n inputs: parsed.inputs && typeof parsed.inputs === 'object' ? parsed.inputs : {},\n ...(typeof parsed.completedAt === 'number' ? { completedAt: parsed.completedAt } : {}),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * A session store backed by a `StorageAdapter`. `ChatController` is synchronous, so\n * the saved session is loaded into memory by `create()`, reads come from memory, and\n * writes are saved in the background. Failures are reported through `onError`.\n *\n * Saves carry the revision they were based on. If another tab wrote first, the two\n * are merged: inputs per question, and progress only when this tab saved progress\n * itself, so a tab that only typed cannot overwrite another tab's progress.\n */\nexport class AdapterSessionManager implements SessionStore {\n private record: SessionRecord;\n private revision: string | undefined;\n private readonly dirtyInputs = new Set<string>();\n private dirtyProgress = false;\n /** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */\n private resetPending = false;\n private pending = false;\n private persisting: Promise<void> | null = null;\n private readonly signal: AbortSignal = new AbortController().signal;\n private unsubscribe: (() => void) | null = null;\n\n private constructor(\n private readonly adapter: StorageAdapter,\n private readonly sessionId: string,\n private readonly available: boolean,\n initial: StoredValue | null,\n private readonly options: AdapterSessionManagerOptions,\n ) {\n const parsed: SessionRecord | null = initial ? parseRecord(initial.value) : null;\n this.record = parsed ?? emptyRecord();\n this.revision = initial?.revision;\n if (initial && !parsed) {\n this.report(\n `Saved session ${sessionId} could not be read as JSON; starting with an empty session`,\n );\n }\n if (available) {\n this.unsubscribe =\n adapter.subscribe?.(keyRecord(sessionId), (stored) => this.applyRemote(stored)) ?? null;\n }\n }\n\n /** Loads (or creates) the session id and its saved record, then returns the manager. */\n static async create(\n adapter: StorageAdapter,\n options: AdapterSessionManagerOptions = {},\n ): Promise<AdapterSessionManager> {\n const signal: AbortSignal = new AbortController().signal;\n const fail = (message: string): void =>\n options.onError?.({ code: 'STORAGE_UNAVAILABLE', message });\n\n let sessionId: string | null = null;\n let available = true;\n\n const idResult: StoreResult<StoredValue | null> = await adapter.get(KEY_SESSION, { signal });\n if (idResult.ok) {\n sessionId = idResult.value?.value ?? null;\n } else {\n available = false;\n fail(`Could not read the session id: ${idResult.error}. Running in memory-only mode`);\n }\n if (available && sessionId === null) {\n sessionId = crypto.randomUUID();\n const saved: StoreResult<StoredValue> = await adapter.set(KEY_SESSION, sessionId, {\n signal,\n });\n if (!saved.ok) {\n available = false;\n fail(`Could not save the session id: ${saved.error}. Running in memory-only mode`);\n }\n }\n const id: string = sessionId ?? crypto.randomUUID();\n\n let initial: StoredValue | null = null;\n if (available) {\n const recordResult: StoreResult<StoredValue | null> = await adapter.get(keyRecord(id), {\n signal,\n });\n if (recordResult.ok) {\n initial = recordResult.value;\n } else {\n available = false;\n fail(`Could not read saved progress: ${recordResult.error}. Running in memory-only mode`);\n }\n }\n return new AdapterSessionManager(adapter, id, available, initial, options);\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n this.record.progress = { currentPageId, answers };\n this.dirtyProgress = true;\n this.schedulePersist();\n }\n\n loadProgress(): SessionProgress | null {\n return this.record.progress;\n }\n\n saveInput(questionId: string, value: InputValue): void {\n this.record.inputs[questionId] = value;\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n loadInput(questionId: string): InputValue | null {\n return this.record.inputs[questionId] ?? null;\n }\n\n clearInput(questionId: string): void {\n if (!(questionId in this.record.inputs)) return;\n delete this.record.inputs[questionId];\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n complete(): void {\n this.record = { progress: null, inputs: {}, completedAt: Date.now() };\n this.dirtyInputs.clear();\n this.dirtyProgress = true;\n this.resetPending = true;\n this.schedulePersist();\n }\n\n completedAt(): number | null {\n return this.record.completedAt ?? null;\n }\n\n clearAll(): void {\n this.record = emptyRecord();\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.resetPending = false;\n this.pending = false;\n if (!this.available) return;\n void this.removeRecord();\n }\n\n dispose(): void {\n this.unsubscribe?.();\n this.unsubscribe = null;\n }\n\n /** Resolves when every save made so far has finished. Mostly for tests. */\n async flush(): Promise<void> {\n while (this.persisting) {\n await this.persisting;\n }\n }\n\n private report(message: string, cause?: unknown): void {\n this.options.onError?.({ code: 'STORAGE_UNAVAILABLE', message, cause });\n }\n\n private schedulePersist(): void {\n if (!this.available) return;\n this.pending = true;\n this.persisting ??= this.drain();\n }\n\n /** Saves until nothing is left to save. Never rejects. */\n private async drain(): Promise<void> {\n let conflicts = 0;\n try {\n while (this.pending) {\n this.pending = false;\n const outcome: 'saved' | 'conflict' | 'failed' = await this.persistOnce();\n conflicts = outcome === 'conflict' ? conflicts + 1 : 0;\n if (outcome === 'conflict') {\n if (conflicts >= MAX_CONFLICT_RETRIES) {\n this.report(\n `Gave up saving session ${this.sessionId} after ${conflicts} revision conflicts in a row`,\n );\n break;\n }\n this.pending = true;\n }\n }\n } finally {\n // Cleared in the same tick the loop ends, so a save requested right now starts a new drain.\n this.persisting = null;\n }\n }\n\n private async persistOnce(): Promise<'saved' | 'conflict' | 'failed'> {\n const sentInputs = new Set<string>(this.dirtyInputs);\n const sentProgress: boolean = this.dirtyProgress;\n const sentReset: boolean = this.resetPending;\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.resetPending = false;\n\n const result: StoreResult<StoredValue> = await this.adapter.set(\n keyRecord(this.sessionId),\n JSON.stringify(this.record),\n { signal: this.signal, ifRevision: this.revision },\n );\n if (result.ok) {\n this.revision = result.value.revision;\n return 'saved';\n }\n\n // Keep what was not saved so the next attempt includes it.\n for (const questionId of sentInputs) this.dirtyInputs.add(questionId);\n this.dirtyProgress = this.dirtyProgress || sentProgress;\n this.resetPending = this.resetPending || sentReset;\n\n if (result.code !== 'conflict') {\n this.report(`Could not save session ${this.sessionId}: ${result.error}`);\n return 'failed';\n }\n const latest: StoreResult<StoredValue | null> = await this.adapter.get(\n keyRecord(this.sessionId),\n { signal: this.signal },\n );\n if (!latest.ok) {\n this.report(\n `Session ${this.sessionId} changed elsewhere, but the new version could not be read: ${latest.error}`,\n );\n return 'failed';\n }\n this.applyRemote(latest.value);\n return 'conflict';\n }\n\n /** Merges a version written elsewhere with this tab's unsaved changes. */\n private applyRemote(stored: StoredValue | null): void {\n const before: string = JSON.stringify(this.record.progress);\n if (!stored) {\n if (!this.dirtyProgress && this.dirtyInputs.size === 0) {\n this.record = emptyRecord();\n }\n this.revision = undefined;\n } else {\n const remote: SessionRecord | null = parseRecord(stored.value);\n if (!remote) {\n this.report(\n `Session ${this.sessionId} was changed elsewhere to a value that is not valid JSON; ignoring it`,\n );\n this.revision = stored.revision;\n return;\n }\n // A finished form leaves nothing of another tab's behind; otherwise the two are merged.\n const inputs: Record<string, InputValue> = this.resetPending ? {} : { ...remote.inputs };\n for (const questionId of this.dirtyInputs) {\n const mine: InputValue | undefined = this.record.inputs[questionId];\n // An input that is dirty but missing here was cleared on this tab.\n if (mine !== undefined) {\n inputs[questionId] = mine;\n } else {\n delete inputs[questionId];\n }\n }\n const completedAt: number | undefined =\n Math.max(this.record.completedAt ?? 0, remote.completedAt ?? 0) || undefined;\n this.record = {\n progress: this.dirtyProgress ? this.record.progress : remote.progress,\n inputs,\n ...(completedAt !== undefined ? { completedAt } : {}),\n };\n this.revision = stored.revision;\n }\n if (JSON.stringify(this.record.progress) !== before) {\n this.options.onExternalChange?.(this.record.progress);\n }\n }\n\n private async removeRecord(): Promise<void> {\n const key: string = keyRecord(this.sessionId);\n const result: StoreResult<unknown> = this.adapter.remove\n ? await this.adapter.remove(key, { signal: this.signal })\n : await this.adapter.set(key, JSON.stringify(emptyRecord()), { signal: this.signal });\n if (result.ok) {\n this.revision = undefined;\n } else {\n this.report(`Could not clear session ${this.sessionId}: ${result.error}`);\n }\n }\n}\n"],"mappings":"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","names":["BaseConsoleErrorAdapter","jsonLogic","KEY_SESSION"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -173,6 +173,14 @@ interface SessionStore {
|
|
|
173
173
|
/** Forgets a saved input, such as a draft that has been submitted. */
|
|
174
174
|
clearInput(questionId: string): void;
|
|
175
175
|
clearAll(): void;
|
|
176
|
+
/**
|
|
177
|
+
* Called when the form is finished. Clears the saved session like `clearAll()` and also records
|
|
178
|
+
* that it was completed, so another tab that is still open can tell "finished" from "cleared".
|
|
179
|
+
* A store without it is cleared with `clearAll()`.
|
|
180
|
+
*/
|
|
181
|
+
complete?(): void;
|
|
182
|
+
/** When `complete()` was last called, in any tab (milliseconds since the epoch), or null. */
|
|
183
|
+
completedAt?(): number | null;
|
|
176
184
|
/** Stops listening for changes made elsewhere. Called when the controller is destroyed. */
|
|
177
185
|
dispose?(): void;
|
|
178
186
|
}
|
|
@@ -188,6 +196,8 @@ declare class SessionManager implements SessionStore {
|
|
|
188
196
|
saveInput(questionId: string, value: string | string[]): void;
|
|
189
197
|
loadInput(questionId: string): string | string[] | null;
|
|
190
198
|
clearInput(questionId: string): void;
|
|
199
|
+
complete(): void;
|
|
200
|
+
completedAt(): number | null;
|
|
191
201
|
clearAll(): void;
|
|
192
202
|
private loadOrCreateSessionId;
|
|
193
203
|
private checkStorage;
|
|
@@ -379,6 +389,8 @@ declare class AdapterSessionManager implements SessionStore {
|
|
|
379
389
|
private revision;
|
|
380
390
|
private readonly dirtyInputs;
|
|
381
391
|
private dirtyProgress;
|
|
392
|
+
/** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
|
|
393
|
+
private resetPending;
|
|
382
394
|
private pending;
|
|
383
395
|
private persisting;
|
|
384
396
|
private readonly signal;
|
|
@@ -393,6 +405,8 @@ declare class AdapterSessionManager implements SessionStore {
|
|
|
393
405
|
saveInput(questionId: string, value: InputValue): void;
|
|
394
406
|
loadInput(questionId: string): InputValue | null;
|
|
395
407
|
clearInput(questionId: string): void;
|
|
408
|
+
complete(): void;
|
|
409
|
+
completedAt(): number | null;
|
|
396
410
|
clearAll(): void;
|
|
397
411
|
dispose(): void;
|
|
398
412
|
/** Resolves when every save made so far has finished. Mostly for tests. */
|
package/dist/index.d.ts
CHANGED
|
@@ -173,6 +173,14 @@ interface SessionStore {
|
|
|
173
173
|
/** Forgets a saved input, such as a draft that has been submitted. */
|
|
174
174
|
clearInput(questionId: string): void;
|
|
175
175
|
clearAll(): void;
|
|
176
|
+
/**
|
|
177
|
+
* Called when the form is finished. Clears the saved session like `clearAll()` and also records
|
|
178
|
+
* that it was completed, so another tab that is still open can tell "finished" from "cleared".
|
|
179
|
+
* A store without it is cleared with `clearAll()`.
|
|
180
|
+
*/
|
|
181
|
+
complete?(): void;
|
|
182
|
+
/** When `complete()` was last called, in any tab (milliseconds since the epoch), or null. */
|
|
183
|
+
completedAt?(): number | null;
|
|
176
184
|
/** Stops listening for changes made elsewhere. Called when the controller is destroyed. */
|
|
177
185
|
dispose?(): void;
|
|
178
186
|
}
|
|
@@ -188,6 +196,8 @@ declare class SessionManager implements SessionStore {
|
|
|
188
196
|
saveInput(questionId: string, value: string | string[]): void;
|
|
189
197
|
loadInput(questionId: string): string | string[] | null;
|
|
190
198
|
clearInput(questionId: string): void;
|
|
199
|
+
complete(): void;
|
|
200
|
+
completedAt(): number | null;
|
|
191
201
|
clearAll(): void;
|
|
192
202
|
private loadOrCreateSessionId;
|
|
193
203
|
private checkStorage;
|
|
@@ -379,6 +389,8 @@ declare class AdapterSessionManager implements SessionStore {
|
|
|
379
389
|
private revision;
|
|
380
390
|
private readonly dirtyInputs;
|
|
381
391
|
private dirtyProgress;
|
|
392
|
+
/** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
|
|
393
|
+
private resetPending;
|
|
382
394
|
private pending;
|
|
383
395
|
private persisting;
|
|
384
396
|
private readonly signal;
|
|
@@ -393,6 +405,8 @@ declare class AdapterSessionManager implements SessionStore {
|
|
|
393
405
|
saveInput(questionId: string, value: InputValue): void;
|
|
394
406
|
loadInput(questionId: string): InputValue | null;
|
|
395
407
|
clearInput(questionId: string): void;
|
|
408
|
+
complete(): void;
|
|
409
|
+
completedAt(): number | null;
|
|
396
410
|
clearAll(): void;
|
|
397
411
|
dispose(): void;
|
|
398
412
|
/** Resolves when every save made so far has finished. Mostly for tests. */
|
package/dist/index.mjs
CHANGED
|
@@ -152,6 +152,7 @@ function makeError(message) {
|
|
|
152
152
|
var KEY_SESSION = "car:session-id";
|
|
153
153
|
var keyProgress = (id) => `car:progress:${id}`;
|
|
154
154
|
var keyInput = (sessionId, questionId) => `car:input:${sessionId}:${questionId}`;
|
|
155
|
+
var keyCompleted = (sessionId) => `car:completed:${sessionId}`;
|
|
155
156
|
var SessionManager = class {
|
|
156
157
|
constructor(storage = globalThis.localStorage) {
|
|
157
158
|
this.storage = storage;
|
|
@@ -196,11 +197,21 @@ var SessionManager = class {
|
|
|
196
197
|
if (!this.available) return;
|
|
197
198
|
this.storage.removeItem(keyInput(this.sessionId, questionId));
|
|
198
199
|
}
|
|
200
|
+
complete() {
|
|
201
|
+
this.clearAll();
|
|
202
|
+
if (!this.available) return;
|
|
203
|
+
this.storage.setItem(keyCompleted(this.sessionId), String(Date.now()));
|
|
204
|
+
}
|
|
205
|
+
completedAt() {
|
|
206
|
+
if (!this.available) return null;
|
|
207
|
+
const completedAt = Number(this.storage.getItem(keyCompleted(this.sessionId)));
|
|
208
|
+
return Number.isFinite(completedAt) && completedAt > 0 ? completedAt : null;
|
|
209
|
+
}
|
|
199
210
|
clearAll() {
|
|
200
211
|
if (!this.available) return;
|
|
201
212
|
const progressKey = keyProgress(this.sessionId);
|
|
202
213
|
const inputPrefix = `car:input:${this.sessionId}:`;
|
|
203
|
-
const keysToRemove = [progressKey];
|
|
214
|
+
const keysToRemove = [progressKey, keyCompleted(this.sessionId)];
|
|
204
215
|
for (let i = 0; i < this.storage.length; i++) {
|
|
205
216
|
const k = this.storage.key(i);
|
|
206
217
|
if (k?.startsWith(inputPrefix)) keysToRemove.push(k);
|
|
@@ -440,7 +451,11 @@ var ChatController = class {
|
|
|
440
451
|
return outcome;
|
|
441
452
|
}
|
|
442
453
|
if (nextPageId === "**FORM_COMPLETED**") {
|
|
443
|
-
this.session.
|
|
454
|
+
if (this.session.complete) {
|
|
455
|
+
this.session.complete();
|
|
456
|
+
} else {
|
|
457
|
+
this.session.clearAll();
|
|
458
|
+
}
|
|
444
459
|
const completeEvent = {
|
|
445
460
|
type: "form:complete",
|
|
446
461
|
sessionId: this.session.getSessionId(),
|
|
@@ -598,7 +613,8 @@ function parseRecord(raw) {
|
|
|
598
613
|
const parsed = JSON.parse(raw);
|
|
599
614
|
return {
|
|
600
615
|
progress: parsed.progress ?? null,
|
|
601
|
-
inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {}
|
|
616
|
+
inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {},
|
|
617
|
+
...typeof parsed.completedAt === "number" ? { completedAt: parsed.completedAt } : {}
|
|
602
618
|
};
|
|
603
619
|
} catch {
|
|
604
620
|
return null;
|
|
@@ -612,6 +628,8 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
612
628
|
this.options = options;
|
|
613
629
|
this.dirtyInputs = /* @__PURE__ */ new Set();
|
|
614
630
|
this.dirtyProgress = false;
|
|
631
|
+
/** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
|
|
632
|
+
this.resetPending = false;
|
|
615
633
|
this.pending = false;
|
|
616
634
|
this.persisting = null;
|
|
617
635
|
this.signal = new AbortController().signal;
|
|
@@ -694,10 +712,21 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
694
712
|
this.dirtyInputs.add(questionId);
|
|
695
713
|
this.schedulePersist();
|
|
696
714
|
}
|
|
715
|
+
complete() {
|
|
716
|
+
this.record = { progress: null, inputs: {}, completedAt: Date.now() };
|
|
717
|
+
this.dirtyInputs.clear();
|
|
718
|
+
this.dirtyProgress = true;
|
|
719
|
+
this.resetPending = true;
|
|
720
|
+
this.schedulePersist();
|
|
721
|
+
}
|
|
722
|
+
completedAt() {
|
|
723
|
+
return this.record.completedAt ?? null;
|
|
724
|
+
}
|
|
697
725
|
clearAll() {
|
|
698
726
|
this.record = emptyRecord();
|
|
699
727
|
this.dirtyInputs.clear();
|
|
700
728
|
this.dirtyProgress = false;
|
|
729
|
+
this.resetPending = false;
|
|
701
730
|
this.pending = false;
|
|
702
731
|
if (!this.available) return;
|
|
703
732
|
void this.removeRecord();
|
|
@@ -745,8 +774,10 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
745
774
|
async persistOnce() {
|
|
746
775
|
const sentInputs = new Set(this.dirtyInputs);
|
|
747
776
|
const sentProgress = this.dirtyProgress;
|
|
777
|
+
const sentReset = this.resetPending;
|
|
748
778
|
this.dirtyInputs.clear();
|
|
749
779
|
this.dirtyProgress = false;
|
|
780
|
+
this.resetPending = false;
|
|
750
781
|
const result = await this.adapter.set(
|
|
751
782
|
keyRecord(this.sessionId),
|
|
752
783
|
JSON.stringify(this.record),
|
|
@@ -758,6 +789,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
758
789
|
}
|
|
759
790
|
for (const questionId of sentInputs) this.dirtyInputs.add(questionId);
|
|
760
791
|
this.dirtyProgress = this.dirtyProgress || sentProgress;
|
|
792
|
+
this.resetPending = this.resetPending || sentReset;
|
|
761
793
|
if (result.code !== "conflict") {
|
|
762
794
|
this.report(`Could not save session ${this.sessionId}: ${result.error}`);
|
|
763
795
|
return "failed";
|
|
@@ -792,7 +824,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
792
824
|
this.revision = stored.revision;
|
|
793
825
|
return;
|
|
794
826
|
}
|
|
795
|
-
const inputs = { ...remote.inputs };
|
|
827
|
+
const inputs = this.resetPending ? {} : { ...remote.inputs };
|
|
796
828
|
for (const questionId of this.dirtyInputs) {
|
|
797
829
|
const mine = this.record.inputs[questionId];
|
|
798
830
|
if (mine !== void 0) {
|
|
@@ -801,9 +833,11 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
801
833
|
delete inputs[questionId];
|
|
802
834
|
}
|
|
803
835
|
}
|
|
836
|
+
const completedAt = Math.max(this.record.completedAt ?? 0, remote.completedAt ?? 0) || void 0;
|
|
804
837
|
this.record = {
|
|
805
838
|
progress: this.dirtyProgress ? this.record.progress : remote.progress,
|
|
806
|
-
inputs
|
|
839
|
+
inputs,
|
|
840
|
+
...completedAt !== void 0 ? { completedAt } : {}
|
|
807
841
|
};
|
|
808
842
|
this.revision = stored.revision;
|
|
809
843
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/adapters/browser-event-adapter.ts","../src/adapters/console-error-adapter.ts","../src/adapters/identity-input-adapter.ts","../src/rule-engine.ts","../src/schema-parser.ts","../src/session-manager.ts","../src/chat-controller.ts","../src/storage/adapter-session-manager.ts"],"sourcesContent":["export type {\n StorageAdapter,\n StoredValue,\n StoreErrorCode,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\n// The browser storage adapters live in the storage-adapters package; re-exported here so\n// they can be imported from core. Server-side adapters (SQLite, files) are in the adapters\n// package.\nexport type {\n LocalStorageAdapterOptions,\n SessionStorageAdapterOptions,\n StorageChangeEvent,\n StorageEventTarget,\n WebStorageAdapterOptions,\n WebStorageLike,\n WebStorageName,\n} from '@richardmcquiston01/storage-adapters/browser';\nexport {\n localStorageAdapter,\n sessionStorageAdapter,\n} from '@richardmcquiston01/storage-adapters/browser';\nexport type { IndexedDbAdapterOptions } from '@richardmcquiston01/storage-adapters/indexeddb';\nexport { indexedDbAdapter } from '@richardmcquiston01/storage-adapters/indexeddb';\nexport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nexport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nexport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nexport { ChatController } from './chat-controller.js';\nexport { RuleEngine } from './rule-engine.js';\nexport { parseSchema } from './schema-parser.js';\nexport type { SessionProgress, SessionStore, StorageBackend } from './session-manager.js';\nexport { SessionManager } from './session-manager.js';\nexport type { AdapterSessionManagerOptions } from './storage/adapter-session-manager.js';\nexport { AdapterSessionManager } from './storage/adapter-session-manager.js';\nexport * from './types.js';\n","import type { ChatEvent, OutputAdapter } from '../types.js';\n\nexport class BrowserEventAdapter implements OutputAdapter {\n emit(event: ChatEvent): void {\n if (typeof document === 'undefined') return;\n const name = event.type === 'page:submit' ? 'chat-page-submit' : 'chat-form-complete';\n document.dispatchEvent(new CustomEvent(name, { detail: event, bubbles: true }));\n }\n}\n","import {\n ConsoleErrorAdapter as BaseConsoleErrorAdapter,\n type ConsoleErrorOptions,\n} from '@richardmcquiston01/error-log-adapter';\nimport type { ChatErrorCode } from '../types.js';\n\n/** Writes errors to `console.error`, prefixed with `[chat-and-react]` unless overridden. */\nexport class ConsoleErrorAdapter extends BaseConsoleErrorAdapter<ChatErrorCode> {\n constructor(options: ConsoleErrorOptions = {}) {\n super({ prefix: '[chat-and-react]', ...options });\n }\n}\n","import type { InputAdapter } from '../types.js';\n\nexport class IdentityInputAdapter implements InputAdapter {\n transform(raw: unknown): unknown {\n return raw;\n }\n}\n","import jsonLogic from 'json-logic-js';\nimport type { AnswerMap, JsonLogicRule } from './types.js';\n\n/**\n * Evaluates JSON Logic rules against provided answers.\n * Wraps json-logic-js to provide type-safe rule evaluation.\n */\nexport class RuleEngine {\n /**\n * Evaluates a JSON Logic rule against an answer map.\n * @param rule The JSON Logic rule to evaluate\n * @param answers The answer map to evaluate the rule against\n * @returns The boolean result of the rule evaluation\n * @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails\n */\n evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean {\n try {\n // json-logic-js apply() returns any; cast to boolean\n return Boolean(\n (jsonLogic as { apply: (rule: unknown, data: unknown) => unknown }).apply(rule, answers),\n );\n } catch (cause) {\n throw { code: 'RULE_EVAL_FAILED', message: 'Rule evaluation failed', cause };\n }\n }\n}\n","import type { ChatPage, ChatSchema } from './types.js';\n\nconst VALID_INPUTS = new Set(['text', 'textarea', 'dropdown', 'radio', 'checkbox']);\nconst FORM_COMPLETED = '**FORM_COMPLETED**';\n\n/**\n * Parses and validates an unknown input as a {@link ChatSchema}.\n *\n * @param raw - The raw input to validate.\n * @returns The typed {@link ChatSchema} if valid.\n * @throws `{ code: 'SCHEMA_INVALID', message: string }` on any validation failure.\n */\nexport function parseSchema(raw: unknown): ChatSchema {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Schema must be a non-null object');\n }\n\n const obj = raw as Record<string, unknown>;\n\n if (obj['x-chat-version'] !== '1') {\n throw makeError('x-chat-version must be \"1\"');\n }\n\n if (!Array.isArray(obj['x-chat-pages'])) {\n throw makeError('x-chat-pages must be an array');\n }\n\n const pages = obj['x-chat-pages'] as unknown[];\n const rules = (\n typeof obj['x-chat-rules'] === 'object' && obj['x-chat-rules'] !== null\n ? obj['x-chat-rules']\n : {}\n ) as Record<string, unknown>;\n\n const pageIds = new Set<string>();\n const questionIds = new Set<string>();\n\n for (const page of pages) {\n validatePage(page, pageIds, questionIds, rules);\n }\n\n // Second pass: validate branching destinations now that all page ids are known\n for (const page of pages) {\n const p = page as ChatPage;\n for (const rule of p.branching) {\n if (rule.destination_id !== FORM_COMPLETED && !pageIds.has(rule.destination_id)) {\n throw makeError(\n `branching destination_id \"${rule.destination_id}\" does not reference a known page id`,\n );\n }\n if (rule.type === 'if' && !questionIds.has(rule.form_element)) {\n throw makeError(\n `branching form_element \"${rule.form_element}\" does not reference a known question id`,\n );\n }\n }\n }\n\n return raw as ChatSchema;\n}\n\n/**\n * Validates a single page object, collecting its id and question ids.\n *\n * @param raw - The raw page value.\n * @param pageIds - Set of already-seen page ids (mutated in place).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validatePage(\n raw: unknown,\n pageIds: Set<string>,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each page must be a non-null object');\n }\n const page = raw as Record<string, unknown>;\n\n if (typeof page['id'] !== 'string' || !page['id']) {\n throw makeError('Page id must be a non-empty string');\n }\n if (pageIds.has(page['id'] as string)) {\n throw makeError(`Duplicate page id \"${page['id']}\"`);\n }\n pageIds.add(page['id'] as string);\n\n if (!Array.isArray(page['questions'])) {\n throw makeError(`Page \"${page['id']}\" must have a questions array`);\n }\n\n const branching = page['branching'];\n if (!Array.isArray(branching) || (branching as unknown[]).length === 0) {\n throw makeError(`Page \"${page['id']}\" must have a non-empty branching array`);\n }\n\n const lastRule = (branching as unknown[])[branching.length - 1] as Record<string, unknown>;\n if (lastRule['type'] !== 'always') {\n throw makeError(`Page \"${page['id']}\" branching must end with a rule of type \"always\"`);\n }\n\n for (const q of page['questions'] as unknown[]) {\n validateQuestion(q, page['id'] as string, questionIds, rules);\n }\n}\n\n/**\n * Validates a single question object, collecting its id.\n *\n * @param raw - The raw question value.\n * @param pageId - The id of the containing page (for error messages).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validateQuestion(\n raw: unknown,\n pageId: string,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each question must be a non-null object');\n }\n const q = raw as Record<string, unknown>;\n\n if (typeof q['id'] !== 'string' || !q['id']) {\n throw makeError(`In page \"${pageId}\": question id must be a non-empty string`);\n }\n if (questionIds.has(q['id'] as string)) {\n throw makeError(`Duplicate question id \"${q['id']}\"`);\n }\n questionIds.add(q['id'] as string);\n\n if (!VALID_INPUTS.has(q['x-chat-input'] as string)) {\n throw makeError(\n `Question \"${q['id']}\" has invalid x-chat-input \"${q['x-chat-input']}\". ` +\n `Must be one of: text, textarea, dropdown, radio, checkbox`,\n );\n }\n\n const condition = q['x-chat-condition'];\n if (condition && typeof condition === 'object' && !Array.isArray(condition)) {\n const c = condition as Record<string, unknown>;\n if ('ref' in c && typeof c['ref'] === 'string') {\n if (!(c['ref'] in rules)) {\n throw makeError(`Question \"${q['id']}\" x-chat-condition refs unknown rule \"${c['ref']}\"`);\n }\n }\n }\n}\n\n/**\n * Creates a schema validation error object.\n *\n * @param message - Human-readable description of the validation failure.\n * @returns An error object with `code: 'SCHEMA_INVALID'`.\n */\nfunction makeError(message: string): { code: 'SCHEMA_INVALID'; message: string } {\n return { code: 'SCHEMA_INVALID', message };\n}\n","// packages/core/src/session-manager.ts\nimport type { AnswerMap } from './types.js';\n\nexport interface StorageBackend {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n key(index: number): string | null;\n readonly length: number;\n}\n\nexport interface SessionProgress {\n currentPageId: string;\n answers: AnswerMap;\n}\n\nconst KEY_SESSION = 'car:session-id';\nconst keyProgress = (id: string) => `car:progress:${id}`;\nconst keyInput = (sessionId: string, questionId: string) => `car:input:${sessionId}:${questionId}`;\n\n/**\n * What `ChatController` needs from session storage. `SessionManager` keeps it in a\n * synchronous Web Storage object; `AdapterSessionManager` keeps it in a `StorageAdapter`.\n */\nexport interface SessionStore {\n readonly isStorageAvailable: boolean;\n getSessionId(): string;\n saveProgress(currentPageId: string, answers: AnswerMap): void;\n loadProgress(): SessionProgress | null;\n saveInput(questionId: string, value: string | string[]): void;\n loadInput(questionId: string): string | string[] | null;\n /** Forgets a saved input, such as a draft that has been submitted. */\n clearInput(questionId: string): void;\n clearAll(): void;\n /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */\n dispose?(): void;\n}\n\nexport class SessionManager implements SessionStore {\n private readonly sessionId: string;\n private readonly available: boolean;\n\n constructor(private readonly storage: StorageBackend = globalThis.localStorage) {\n this.available = this.checkStorage();\n this.sessionId = this.loadOrCreateSessionId();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n if (!this.available) return;\n this.storage.setItem(keyProgress(this.sessionId), JSON.stringify({ currentPageId, answers }));\n }\n\n loadProgress(): SessionProgress | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyProgress(this.sessionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as SessionProgress;\n } catch {\n return null;\n }\n }\n\n saveInput(questionId: string, value: string | string[]): void {\n if (!this.available) return;\n this.storage.setItem(keyInput(this.sessionId, questionId), JSON.stringify(value));\n }\n\n loadInput(questionId: string): string | string[] | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyInput(this.sessionId, questionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as string | string[];\n } catch {\n return null;\n }\n }\n\n clearInput(questionId: string): void {\n if (!this.available) return;\n this.storage.removeItem(keyInput(this.sessionId, questionId));\n }\n\n clearAll(): void {\n if (!this.available) return;\n const progressKey = keyProgress(this.sessionId);\n const inputPrefix = `car:input:${this.sessionId}:`;\n const keysToRemove: string[] = [progressKey];\n\n for (let i = 0; i < this.storage.length; i++) {\n const k = this.storage.key(i);\n if (k?.startsWith(inputPrefix)) keysToRemove.push(k);\n }\n\n for (const k of keysToRemove) {\n this.storage.removeItem(k);\n }\n }\n\n private loadOrCreateSessionId(): string {\n if (!this.available) return crypto.randomUUID();\n const existing = this.storage.getItem(KEY_SESSION);\n if (existing) return existing;\n const id = crypto.randomUUID();\n this.storage.setItem(KEY_SESSION, id);\n return id;\n }\n\n private checkStorage(): boolean {\n try {\n const probe = '__car_probe__';\n this.storage.setItem(probe, '1');\n this.storage.removeItem(probe);\n return true;\n } catch {\n return false;\n }\n }\n}\n","// packages/core/src/chat-controller.ts\n\nimport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nimport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nimport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nimport { RuleEngine } from './rule-engine.js';\nimport { parseSchema } from './schema-parser.js';\nimport { SessionManager, type SessionStore } from './session-manager.js';\nimport type {\n AnswerMap,\n ChatControllerConfig,\n ChatError,\n ChatEvent,\n ChatOptions,\n ChatPage,\n ChatQuestion,\n ChatSchema,\n ErrorLogResult,\n RouteEntry,\n SubmitOutcome,\n} from './types.js';\n\ntype EventName = 'page:submit' | 'form:complete';\ntype Listener = (event: ChatEvent) => void;\n\nconst DEFAULT_OPTIONS: ChatOptions = {\n userResumable: true,\n localStorage: true,\n autoComplete: true,\n allowEdit: false,\n autoHideQuestions: 0,\n autoHideAnswers: 0,\n hideAnimation: 'fade',\n typingSpeed: 0,\n thinkingDelay: 0,\n thinkingText: '',\n showContinueIcon: true,\n};\n\n/** Copies an object without its undefined values, so they cannot hide a default. */\nfunction stripUndefined<T extends object>(source: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(source).filter(([, value]) => value !== undefined),\n ) as Partial<T>;\n}\n\n/**\n * Central orchestrator for the chat-and-react form flow.\n * Drives page navigation, branching, session persistence, and event emission.\n */\nexport class ChatController {\n private schema: ChatSchema | null = null;\n private options: ChatOptions = { ...DEFAULT_OPTIONS };\n private readonly config: Required<ChatControllerConfig>;\n private readonly session: SessionStore;\n private readonly ruleEngine = new RuleEngine();\n private currentPage: ChatPage | null = null;\n private allAnswers: AnswerMap = {};\n private visitIndex = 0;\n /** Pages submitted in this session, in visit order. Used for editing earlier answers. */\n private route: RouteEntry[] = [];\n /** Index in `route` of the page being edited, or null when not editing. */\n private editIndex: number | null = null;\n /** The page the user was on before they started editing an earlier one. */\n private resumePageId: string | null = null;\n private readonly listeners = new Map<EventName, Set<Listener>>();\n\n /**\n * @param config - Controller configuration including schema and optional adapters.\n * @param sessionManager - Optional session manager for test injection.\n */\n constructor(config: ChatControllerConfig, sessionManager?: SessionStore) {\n this.config = {\n inputAdapter: config.inputAdapter ?? new IdentityInputAdapter(),\n outputAdapter: config.outputAdapter ?? new BrowserEventAdapter(),\n errorAdapter: config.errorAdapter ?? new ConsoleErrorAdapter(),\n schema: config.schema,\n options: config.options ?? {},\n };\n this.session = sessionManager ?? new SessionManager();\n }\n\n /**\n * Initialises the controller: parses the schema, resolves options,\n * restores any saved session, and positions to the first page.\n */\n start(): void {\n let raw: unknown;\n try {\n raw = this.config.inputAdapter.transform(this.config.schema);\n } catch (cause) {\n this.handleFatal({ code: 'ADAPTER_ERROR', message: 'InputAdapter.transform() threw', cause });\n }\n\n let schema: ChatSchema;\n try {\n schema = parseSchema(raw);\n } catch (e) {\n this.handleFatal(e as ChatError);\n }\n this.schema = schema;\n\n if (!this.session.isStorageAvailable) {\n this.logError({\n code: 'STORAGE_UNAVAILABLE',\n message: 'localStorage unavailable; running in memory-only mode',\n });\n }\n\n // Defaults, then the schema, then overrides passed in by the host (for\n // example element attributes).\n const schemaOpts = schema['x-chat-options'] ?? {};\n this.options = {\n ...DEFAULT_OPTIONS,\n ...stripUndefined(schemaOpts),\n ...stripUndefined(this.config.options),\n };\n\n if (this.options.userResumable) {\n const saved = this.session.loadProgress();\n if (saved) {\n const page = schema['x-chat-pages'].find((p) => p.id === saved.currentPageId);\n if (page) {\n this.allAnswers = saved.answers;\n this.currentPage = page;\n return;\n }\n }\n }\n\n this.currentPage = schema['x-chat-pages'][0];\n }\n\n /**\n * Returns the page currently being presented to the user, or null if complete.\n */\n getCurrentPage(): ChatPage | null {\n return this.currentPage;\n }\n\n /**\n * Returns the subset of questions on the current page that pass their\n * `x-chat-condition` rule (if any). Questions without a condition are always visible.\n */\n getVisibleQuestions(): ChatQuestion[] {\n if (!this.currentPage) return [];\n return this.currentPage.questions.filter((q) => this.isQuestionVisible(q));\n }\n\n /**\n * Returns the resolved options for the current session (schema options merged with defaults).\n */\n getOptions(): ChatOptions {\n return { ...this.options };\n }\n\n /**\n * Returns the pages the user has submitted in this session, in visit order.\n * A session restored from storage starts with an empty route.\n */\n getRoute(): RouteEntry[] {\n return this.route.map((entry) => ({ pageId: entry.pageId, answers: { ...entry.answers } }));\n }\n\n /**\n * Returns every answer given so far, for pre-filling inputs.\n */\n getAnswers(): AnswerMap {\n return { ...this.allAnswers };\n }\n\n /**\n * Re-opens a page the user already submitted so its answers can be changed.\n * Call `submitPage` afterwards to save the new answers. Not available once the\n * form is complete.\n *\n * @param routeIndex - Position of the page in `getRoute()`.\n * @returns true when the page was re-opened.\n */\n reopenPage(routeIndex: number): boolean {\n if (!this.schema || !this.currentPage) return false;\n const entry = this.route[routeIndex];\n if (!entry) return false;\n const page = this.schema['x-chat-pages'].find((p) => p.id === entry.pageId);\n if (!page) return false;\n\n // Remember where the user was, but only the first time: editing another\n // earlier page before finishing this edit must still return to that page.\n if (this.editIndex === null) this.resumePageId = this.currentPage.id;\n this.editIndex = routeIndex;\n this.currentPage = page;\n return true;\n }\n\n /**\n * Submits the answers for the current page and advances the session.\n * - Accumulates answers into `allAnswers`.\n * - Emits `page:submit` (suppressed when `userResumable` is false).\n * - Resolves branching to find the next page.\n * - Emits `form:complete` and clears session when `**FORM_COMPLETED**` is reached.\n *\n * When the page was re-opened with `reopenPage`, the new answers replace the\n * old ones and `page:submit` is emitted with `edited: true`. If the page's\n * branching still leads to the same next page, the form returns to the page the\n * user was on. If it leads somewhere else, the pages that followed it are\n * dropped, along with their answers, and the form continues from the new page.\n *\n * @param pageAnswers - Answers collected for the current page.\n */\n submitPage(pageAnswers: AnswerMap): SubmitOutcome {\n if (!this.currentPage || !this.schema) return 'advanced';\n\n const pageId = this.currentPage.id;\n const currentVisitIndex = this.visitIndex;\n const editedIndex = this.editIndex;\n const isEdit = editedIndex !== null;\n\n let outcome: SubmitOutcome = 'advanced';\n let nextPageId: string;\n let resumePageId: string | null = null;\n\n if (isEdit) {\n const entry = this.route[editedIndex];\n // Replace the old answers for this page, including any question that is\n // no longer visible after the change.\n const remaining = { ...this.allAnswers };\n for (const questionId of Object.keys(entry.answers)) delete remaining[questionId];\n this.allAnswers = { ...remaining, ...pageAnswers };\n entry.answers = { ...pageAnswers };\n\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n const expectedNextPageId = this.route[editedIndex + 1]?.pageId ?? this.resumePageId;\n\n if (nextPageId === expectedNextPageId) {\n outcome = 'edit-resumed';\n resumePageId = this.resumePageId;\n } else {\n outcome = 'edit-rerouted';\n // The route changed: forget the pages that followed and their answers.\n for (const later of this.route.slice(editedIndex + 1)) {\n for (const questionId of Object.keys(later.answers)) delete this.allAnswers[questionId];\n }\n this.route.length = editedIndex + 1;\n }\n this.editIndex = null;\n this.resumePageId = null;\n } else {\n this.allAnswers = { ...this.allAnswers, ...pageAnswers };\n this.route.push({ pageId, answers: { ...pageAnswers } });\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n }\n\n if (this.options.userResumable) {\n const progressPageId =\n outcome === 'edit-resumed' && resumePageId\n ? resumePageId\n : nextPageId === '**FORM_COMPLETED**'\n ? pageId\n : nextPageId;\n this.session.saveProgress(progressPageId, this.allAnswers);\n\n const submitEvent: ChatEvent = {\n type: 'page:submit',\n sessionId: this.session.getSessionId(),\n pageId,\n visitIndex: currentVisitIndex,\n answers: pageAnswers,\n ...(isEdit ? { edited: true } : {}),\n };\n this.safeEmit(submitEvent);\n this.emit('page:submit', submitEvent);\n }\n\n this.visitIndex++;\n\n if (outcome === 'edit-resumed' && resumePageId) {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === resumePageId) ?? null;\n return outcome;\n }\n\n if (nextPageId === '**FORM_COMPLETED**') {\n this.session.clearAll();\n const completeEvent: ChatEvent = {\n type: 'form:complete',\n sessionId: this.session.getSessionId(),\n allAnswers: this.allAnswers,\n };\n this.safeEmit(completeEvent);\n this.emit('form:complete', completeEvent);\n this.currentPage = null;\n } else {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === nextPageId) ?? null;\n }\n return outcome;\n }\n\n /**\n * Registers an event listener for the given event type.\n *\n * @param event - The event to listen for.\n * @param listener - The callback to invoke when the event fires.\n */\n on(event: EventName, listener: Listener): void {\n let listenersForEvent = this.listeners.get(event);\n if (!listenersForEvent) {\n listenersForEvent = new Set();\n this.listeners.set(event, listenersForEvent);\n }\n listenersForEvent.add(listener);\n }\n\n /**\n * Removes a previously registered event listener.\n *\n * @param event - The event type to deregister from.\n * @param listener - The specific listener to remove.\n */\n off(event: EventName, listener: Listener): void {\n this.listeners.get(event)?.delete(listener);\n }\n\n /**\n * Tears down the controller: removes all listeners and nulls internal state.\n */\n destroy(): void {\n this.session.dispose?.();\n this.listeners.clear();\n this.currentPage = null;\n this.schema = null;\n }\n\n /**\n * Evaluates whether a question should be shown based on its `x-chat-condition`.\n * Returns true when no condition is present. On evaluation failure, logs and returns false.\n */\n private isQuestionVisible(question: ChatQuestion): boolean {\n const condition = question['x-chat-condition'];\n if (!condition) return true;\n\n const rules = this.schema?.['x-chat-rules'] ?? {};\n let rule: Record<string, unknown>;\n\n if ('ref' in condition && typeof (condition as { ref?: unknown }).ref === 'string') {\n const ref = (condition as { ref: string }).ref;\n rule = rules[ref];\n if (!rule) return false;\n } else {\n rule = condition as Record<string, unknown>;\n }\n\n try {\n return this.ruleEngine.evaluate(rule, this.answersForRules());\n } catch (e) {\n this.logError(e as ChatError);\n return false;\n }\n }\n\n /**\n * The answers question conditions are checked against. While an earlier page is\n * being edited, that page's own answers are left out, so its questions are\n * shown exactly as they were the first time the page was asked.\n */\n private answersForRules(): AnswerMap {\n if (this.editIndex === null) return this.allAnswers;\n const rest = { ...this.allAnswers };\n for (const questionId of Object.keys(this.route[this.editIndex].answers))\n delete rest[questionId];\n return rest;\n }\n\n /**\n * Walks the branching rules top-to-bottom. Returns the first match's destination_id.\n * Falls back to `**FORM_COMPLETED**` if nothing matches (should not happen with valid schemas).\n */\n private resolveBranching(page: ChatPage, answers: AnswerMap): string {\n for (const rule of page.branching) {\n if (rule.type === 'always') return rule.destination_id;\n if (rule.type === 'if' && answers[rule.form_element] === rule.value) {\n return rule.destination_id;\n }\n }\n return '**FORM_COMPLETED**';\n }\n\n /**\n * Calls `outputAdapter.emit()` and swallows adapter errors (logging them instead).\n */\n private safeEmit(event: ChatEvent): void {\n try {\n this.config.outputAdapter.emit(event);\n } catch (cause) {\n this.logError({\n code: 'ADAPTER_ERROR',\n message: `OutputAdapter.emit() threw for ${event.type}`,\n cause,\n });\n }\n }\n\n /**\n * Notifies all registered listeners for the given event.\n */\n private emit(event: EventName, payload: ChatEvent): void {\n this.listeners.get(event)?.forEach((l) => {\n l(payload);\n });\n }\n\n /**\n * Logs the error via the error adapter and then throws it.\n * Used for unrecoverable errors (schema invalid, adapter failure on start).\n */\n private handleFatal(error: ChatError): never {\n this.logError(error);\n throw error;\n }\n\n /**\n * Sends the error to the error adapter without waiting for it. If the adapter\n * reports a failure, rejects, or throws, that is written to `console.error` so\n * the original error is never lost and the form keeps running.\n */\n private logError(entry: ChatError): void {\n const reportFailure = (reason: unknown): void => {\n console.error('[chat-and-react] ErrorLogAdapter failed to record an error:', reason, entry);\n };\n try {\n // `Promise.resolve` also tolerates older custom adapters that return nothing.\n void Promise.resolve(this.config.errorAdapter.log(entry)).then(\n (result: ErrorLogResult | undefined): void => {\n if (result?.ok === false) {\n reportFailure(result.error);\n }\n },\n reportFailure,\n );\n } catch (cause) {\n reportFailure(cause);\n }\n }\n}\n","import type {\n StorageAdapter,\n StoredValue,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\nimport type { SessionProgress, SessionStore } from '../session-manager.js';\nimport type { AnswerMap, ChatError } from '../types.js';\n\nconst KEY_SESSION = 'car:session-id';\nconst keyRecord = (sessionId: string): string => `car:session:${sessionId}`;\n\n/** After this many revision conflicts in a row, a save is abandoned and reported. */\nconst MAX_CONFLICT_RETRIES = 3;\n\ntype InputValue = string | string[];\n\n/** Everything saved for one session, stored under a single key. */\ninterface SessionRecord {\n progress: SessionProgress | null;\n inputs: Record<string, InputValue>;\n}\n\nexport interface AdapterSessionManagerOptions {\n /** Called when the adapter fails. The session keeps working from memory. */\n onError?: (error: ChatError) => void;\n /**\n * Called when saved progress changes because of a write made elsewhere, such as\n * another tab. `null` means the session was cleared there.\n */\n onExternalChange?: (progress: SessionProgress | null) => void;\n}\n\nconst emptyRecord = (): SessionRecord => ({ progress: null, inputs: {} });\n\nfunction parseRecord(raw: string): SessionRecord | null {\n try {\n const parsed = JSON.parse(raw) as Partial<SessionRecord>;\n return {\n progress: parsed.progress ?? null,\n inputs: parsed.inputs && typeof parsed.inputs === 'object' ? parsed.inputs : {},\n };\n } catch {\n return null;\n }\n}\n\n/**\n * A session store backed by a `StorageAdapter`. `ChatController` is synchronous, so\n * the saved session is loaded into memory by `create()`, reads come from memory, and\n * writes are saved in the background. Failures are reported through `onError`.\n *\n * Saves carry the revision they were based on. If another tab wrote first, the two\n * are merged: inputs per question, and progress only when this tab saved progress\n * itself, so a tab that only typed cannot overwrite another tab's progress.\n */\nexport class AdapterSessionManager implements SessionStore {\n private record: SessionRecord;\n private revision: string | undefined;\n private readonly dirtyInputs = new Set<string>();\n private dirtyProgress = false;\n private pending = false;\n private persisting: Promise<void> | null = null;\n private readonly signal: AbortSignal = new AbortController().signal;\n private unsubscribe: (() => void) | null = null;\n\n private constructor(\n private readonly adapter: StorageAdapter,\n private readonly sessionId: string,\n private readonly available: boolean,\n initial: StoredValue | null,\n private readonly options: AdapterSessionManagerOptions,\n ) {\n const parsed: SessionRecord | null = initial ? parseRecord(initial.value) : null;\n this.record = parsed ?? emptyRecord();\n this.revision = initial?.revision;\n if (initial && !parsed) {\n this.report(\n `Saved session ${sessionId} could not be read as JSON; starting with an empty session`,\n );\n }\n if (available) {\n this.unsubscribe =\n adapter.subscribe?.(keyRecord(sessionId), (stored) => this.applyRemote(stored)) ?? null;\n }\n }\n\n /** Loads (or creates) the session id and its saved record, then returns the manager. */\n static async create(\n adapter: StorageAdapter,\n options: AdapterSessionManagerOptions = {},\n ): Promise<AdapterSessionManager> {\n const signal: AbortSignal = new AbortController().signal;\n const fail = (message: string): void =>\n options.onError?.({ code: 'STORAGE_UNAVAILABLE', message });\n\n let sessionId: string | null = null;\n let available = true;\n\n const idResult: StoreResult<StoredValue | null> = await adapter.get(KEY_SESSION, { signal });\n if (idResult.ok) {\n sessionId = idResult.value?.value ?? null;\n } else {\n available = false;\n fail(`Could not read the session id: ${idResult.error}. Running in memory-only mode`);\n }\n if (available && sessionId === null) {\n sessionId = crypto.randomUUID();\n const saved: StoreResult<StoredValue> = await adapter.set(KEY_SESSION, sessionId, {\n signal,\n });\n if (!saved.ok) {\n available = false;\n fail(`Could not save the session id: ${saved.error}. Running in memory-only mode`);\n }\n }\n const id: string = sessionId ?? crypto.randomUUID();\n\n let initial: StoredValue | null = null;\n if (available) {\n const recordResult: StoreResult<StoredValue | null> = await adapter.get(keyRecord(id), {\n signal,\n });\n if (recordResult.ok) {\n initial = recordResult.value;\n } else {\n available = false;\n fail(`Could not read saved progress: ${recordResult.error}. Running in memory-only mode`);\n }\n }\n return new AdapterSessionManager(adapter, id, available, initial, options);\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n this.record.progress = { currentPageId, answers };\n this.dirtyProgress = true;\n this.schedulePersist();\n }\n\n loadProgress(): SessionProgress | null {\n return this.record.progress;\n }\n\n saveInput(questionId: string, value: InputValue): void {\n this.record.inputs[questionId] = value;\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n loadInput(questionId: string): InputValue | null {\n return this.record.inputs[questionId] ?? null;\n }\n\n clearInput(questionId: string): void {\n if (!(questionId in this.record.inputs)) return;\n delete this.record.inputs[questionId];\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n clearAll(): void {\n this.record = emptyRecord();\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.pending = false;\n if (!this.available) return;\n void this.removeRecord();\n }\n\n dispose(): void {\n this.unsubscribe?.();\n this.unsubscribe = null;\n }\n\n /** Resolves when every save made so far has finished. Mostly for tests. */\n async flush(): Promise<void> {\n while (this.persisting) {\n await this.persisting;\n }\n }\n\n private report(message: string, cause?: unknown): void {\n this.options.onError?.({ code: 'STORAGE_UNAVAILABLE', message, cause });\n }\n\n private schedulePersist(): void {\n if (!this.available) return;\n this.pending = true;\n this.persisting ??= this.drain();\n }\n\n /** Saves until nothing is left to save. Never rejects. */\n private async drain(): Promise<void> {\n let conflicts = 0;\n try {\n while (this.pending) {\n this.pending = false;\n const outcome: 'saved' | 'conflict' | 'failed' = await this.persistOnce();\n conflicts = outcome === 'conflict' ? conflicts + 1 : 0;\n if (outcome === 'conflict') {\n if (conflicts >= MAX_CONFLICT_RETRIES) {\n this.report(\n `Gave up saving session ${this.sessionId} after ${conflicts} revision conflicts in a row`,\n );\n break;\n }\n this.pending = true;\n }\n }\n } finally {\n // Cleared in the same tick the loop ends, so a save requested right now starts a new drain.\n this.persisting = null;\n }\n }\n\n private async persistOnce(): Promise<'saved' | 'conflict' | 'failed'> {\n const sentInputs = new Set<string>(this.dirtyInputs);\n const sentProgress: boolean = this.dirtyProgress;\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n\n const result: StoreResult<StoredValue> = await this.adapter.set(\n keyRecord(this.sessionId),\n JSON.stringify(this.record),\n { signal: this.signal, ifRevision: this.revision },\n );\n if (result.ok) {\n this.revision = result.value.revision;\n return 'saved';\n }\n\n // Keep what was not saved so the next attempt includes it.\n for (const questionId of sentInputs) this.dirtyInputs.add(questionId);\n this.dirtyProgress = this.dirtyProgress || sentProgress;\n\n if (result.code !== 'conflict') {\n this.report(`Could not save session ${this.sessionId}: ${result.error}`);\n return 'failed';\n }\n const latest: StoreResult<StoredValue | null> = await this.adapter.get(\n keyRecord(this.sessionId),\n { signal: this.signal },\n );\n if (!latest.ok) {\n this.report(\n `Session ${this.sessionId} changed elsewhere, but the new version could not be read: ${latest.error}`,\n );\n return 'failed';\n }\n this.applyRemote(latest.value);\n return 'conflict';\n }\n\n /** Merges a version written elsewhere with this tab's unsaved changes. */\n private applyRemote(stored: StoredValue | null): void {\n const before: string = JSON.stringify(this.record.progress);\n if (!stored) {\n if (!this.dirtyProgress && this.dirtyInputs.size === 0) {\n this.record = emptyRecord();\n }\n this.revision = undefined;\n } else {\n const remote: SessionRecord | null = parseRecord(stored.value);\n if (!remote) {\n this.report(\n `Session ${this.sessionId} was changed elsewhere to a value that is not valid JSON; ignoring it`,\n );\n this.revision = stored.revision;\n return;\n }\n const inputs: Record<string, InputValue> = { ...remote.inputs };\n for (const questionId of this.dirtyInputs) {\n const mine: InputValue | undefined = this.record.inputs[questionId];\n // An input that is dirty but missing here was cleared on this tab.\n if (mine !== undefined) {\n inputs[questionId] = mine;\n } else {\n delete inputs[questionId];\n }\n }\n this.record = {\n progress: this.dirtyProgress ? this.record.progress : remote.progress,\n inputs,\n };\n this.revision = stored.revision;\n }\n if (JSON.stringify(this.record.progress) !== before) {\n this.options.onExternalChange?.(this.record.progress);\n }\n }\n\n private async removeRecord(): Promise<void> {\n const key: string = keyRecord(this.sessionId);\n const result: StoreResult<unknown> = this.adapter.remove\n ? await this.adapter.remove(key, { signal: this.signal })\n : await this.adapter.set(key, JSON.stringify(emptyRecord()), { signal: this.signal });\n if (result.ok) {\n this.revision = undefined;\n } else {\n this.report(`Could not clear session ${this.sessionId}: ${result.error}`);\n }\n }\n}\n"],"mappings":"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","names":["KEY_SESSION"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/adapters/browser-event-adapter.ts","../src/adapters/console-error-adapter.ts","../src/adapters/identity-input-adapter.ts","../src/rule-engine.ts","../src/schema-parser.ts","../src/session-manager.ts","../src/chat-controller.ts","../src/storage/adapter-session-manager.ts"],"sourcesContent":["export type {\n StorageAdapter,\n StoredValue,\n StoreErrorCode,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\n// The browser storage adapters live in the storage-adapters package; re-exported here so\n// they can be imported from core. Server-side adapters (SQLite, files) are in the adapters\n// package.\nexport type {\n LocalStorageAdapterOptions,\n SessionStorageAdapterOptions,\n StorageChangeEvent,\n StorageEventTarget,\n WebStorageAdapterOptions,\n WebStorageLike,\n WebStorageName,\n} from '@richardmcquiston01/storage-adapters/browser';\nexport {\n localStorageAdapter,\n sessionStorageAdapter,\n} from '@richardmcquiston01/storage-adapters/browser';\nexport type { IndexedDbAdapterOptions } from '@richardmcquiston01/storage-adapters/indexeddb';\nexport { indexedDbAdapter } from '@richardmcquiston01/storage-adapters/indexeddb';\nexport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nexport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nexport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nexport { ChatController } from './chat-controller.js';\nexport { RuleEngine } from './rule-engine.js';\nexport { parseSchema } from './schema-parser.js';\nexport type { SessionProgress, SessionStore, StorageBackend } from './session-manager.js';\nexport { SessionManager } from './session-manager.js';\nexport type { AdapterSessionManagerOptions } from './storage/adapter-session-manager.js';\nexport { AdapterSessionManager } from './storage/adapter-session-manager.js';\nexport * from './types.js';\n","import type { ChatEvent, OutputAdapter } from '../types.js';\n\nexport class BrowserEventAdapter implements OutputAdapter {\n emit(event: ChatEvent): void {\n if (typeof document === 'undefined') return;\n const name = event.type === 'page:submit' ? 'chat-page-submit' : 'chat-form-complete';\n document.dispatchEvent(new CustomEvent(name, { detail: event, bubbles: true }));\n }\n}\n","import {\n ConsoleErrorAdapter as BaseConsoleErrorAdapter,\n type ConsoleErrorOptions,\n} from '@richardmcquiston01/error-log-adapter';\nimport type { ChatErrorCode } from '../types.js';\n\n/** Writes errors to `console.error`, prefixed with `[chat-and-react]` unless overridden. */\nexport class ConsoleErrorAdapter extends BaseConsoleErrorAdapter<ChatErrorCode> {\n constructor(options: ConsoleErrorOptions = {}) {\n super({ prefix: '[chat-and-react]', ...options });\n }\n}\n","import type { InputAdapter } from '../types.js';\n\nexport class IdentityInputAdapter implements InputAdapter {\n transform(raw: unknown): unknown {\n return raw;\n }\n}\n","import jsonLogic from 'json-logic-js';\nimport type { AnswerMap, JsonLogicRule } from './types.js';\n\n/**\n * Evaluates JSON Logic rules against provided answers.\n * Wraps json-logic-js to provide type-safe rule evaluation.\n */\nexport class RuleEngine {\n /**\n * Evaluates a JSON Logic rule against an answer map.\n * @param rule The JSON Logic rule to evaluate\n * @param answers The answer map to evaluate the rule against\n * @returns The boolean result of the rule evaluation\n * @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails\n */\n evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean {\n try {\n // json-logic-js apply() returns any; cast to boolean\n return Boolean(\n (jsonLogic as { apply: (rule: unknown, data: unknown) => unknown }).apply(rule, answers),\n );\n } catch (cause) {\n throw { code: 'RULE_EVAL_FAILED', message: 'Rule evaluation failed', cause };\n }\n }\n}\n","import type { ChatPage, ChatSchema } from './types.js';\n\nconst VALID_INPUTS = new Set(['text', 'textarea', 'dropdown', 'radio', 'checkbox']);\nconst FORM_COMPLETED = '**FORM_COMPLETED**';\n\n/**\n * Parses and validates an unknown input as a {@link ChatSchema}.\n *\n * @param raw - The raw input to validate.\n * @returns The typed {@link ChatSchema} if valid.\n * @throws `{ code: 'SCHEMA_INVALID', message: string }` on any validation failure.\n */\nexport function parseSchema(raw: unknown): ChatSchema {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Schema must be a non-null object');\n }\n\n const obj = raw as Record<string, unknown>;\n\n if (obj['x-chat-version'] !== '1') {\n throw makeError('x-chat-version must be \"1\"');\n }\n\n if (!Array.isArray(obj['x-chat-pages'])) {\n throw makeError('x-chat-pages must be an array');\n }\n\n const pages = obj['x-chat-pages'] as unknown[];\n const rules = (\n typeof obj['x-chat-rules'] === 'object' && obj['x-chat-rules'] !== null\n ? obj['x-chat-rules']\n : {}\n ) as Record<string, unknown>;\n\n const pageIds = new Set<string>();\n const questionIds = new Set<string>();\n\n for (const page of pages) {\n validatePage(page, pageIds, questionIds, rules);\n }\n\n // Second pass: validate branching destinations now that all page ids are known\n for (const page of pages) {\n const p = page as ChatPage;\n for (const rule of p.branching) {\n if (rule.destination_id !== FORM_COMPLETED && !pageIds.has(rule.destination_id)) {\n throw makeError(\n `branching destination_id \"${rule.destination_id}\" does not reference a known page id`,\n );\n }\n if (rule.type === 'if' && !questionIds.has(rule.form_element)) {\n throw makeError(\n `branching form_element \"${rule.form_element}\" does not reference a known question id`,\n );\n }\n }\n }\n\n return raw as ChatSchema;\n}\n\n/**\n * Validates a single page object, collecting its id and question ids.\n *\n * @param raw - The raw page value.\n * @param pageIds - Set of already-seen page ids (mutated in place).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validatePage(\n raw: unknown,\n pageIds: Set<string>,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each page must be a non-null object');\n }\n const page = raw as Record<string, unknown>;\n\n if (typeof page['id'] !== 'string' || !page['id']) {\n throw makeError('Page id must be a non-empty string');\n }\n if (pageIds.has(page['id'] as string)) {\n throw makeError(`Duplicate page id \"${page['id']}\"`);\n }\n pageIds.add(page['id'] as string);\n\n if (!Array.isArray(page['questions'])) {\n throw makeError(`Page \"${page['id']}\" must have a questions array`);\n }\n\n const branching = page['branching'];\n if (!Array.isArray(branching) || (branching as unknown[]).length === 0) {\n throw makeError(`Page \"${page['id']}\" must have a non-empty branching array`);\n }\n\n const lastRule = (branching as unknown[])[branching.length - 1] as Record<string, unknown>;\n if (lastRule['type'] !== 'always') {\n throw makeError(`Page \"${page['id']}\" branching must end with a rule of type \"always\"`);\n }\n\n for (const q of page['questions'] as unknown[]) {\n validateQuestion(q, page['id'] as string, questionIds, rules);\n }\n}\n\n/**\n * Validates a single question object, collecting its id.\n *\n * @param raw - The raw question value.\n * @param pageId - The id of the containing page (for error messages).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validateQuestion(\n raw: unknown,\n pageId: string,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each question must be a non-null object');\n }\n const q = raw as Record<string, unknown>;\n\n if (typeof q['id'] !== 'string' || !q['id']) {\n throw makeError(`In page \"${pageId}\": question id must be a non-empty string`);\n }\n if (questionIds.has(q['id'] as string)) {\n throw makeError(`Duplicate question id \"${q['id']}\"`);\n }\n questionIds.add(q['id'] as string);\n\n if (!VALID_INPUTS.has(q['x-chat-input'] as string)) {\n throw makeError(\n `Question \"${q['id']}\" has invalid x-chat-input \"${q['x-chat-input']}\". ` +\n `Must be one of: text, textarea, dropdown, radio, checkbox`,\n );\n }\n\n const condition = q['x-chat-condition'];\n if (condition && typeof condition === 'object' && !Array.isArray(condition)) {\n const c = condition as Record<string, unknown>;\n if ('ref' in c && typeof c['ref'] === 'string') {\n if (!(c['ref'] in rules)) {\n throw makeError(`Question \"${q['id']}\" x-chat-condition refs unknown rule \"${c['ref']}\"`);\n }\n }\n }\n}\n\n/**\n * Creates a schema validation error object.\n *\n * @param message - Human-readable description of the validation failure.\n * @returns An error object with `code: 'SCHEMA_INVALID'`.\n */\nfunction makeError(message: string): { code: 'SCHEMA_INVALID'; message: string } {\n return { code: 'SCHEMA_INVALID', message };\n}\n","// packages/core/src/session-manager.ts\nimport type { AnswerMap } from './types.js';\n\nexport interface StorageBackend {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n key(index: number): string | null;\n readonly length: number;\n}\n\nexport interface SessionProgress {\n currentPageId: string;\n answers: AnswerMap;\n}\n\nconst KEY_SESSION = 'car:session-id';\nconst keyProgress = (id: string) => `car:progress:${id}`;\nconst keyInput = (sessionId: string, questionId: string) => `car:input:${sessionId}:${questionId}`;\nconst keyCompleted = (sessionId: string) => `car:completed:${sessionId}`;\n\n/**\n * What `ChatController` needs from session storage. `SessionManager` keeps it in a\n * synchronous Web Storage object; `AdapterSessionManager` keeps it in a `StorageAdapter`.\n */\nexport interface SessionStore {\n readonly isStorageAvailable: boolean;\n getSessionId(): string;\n saveProgress(currentPageId: string, answers: AnswerMap): void;\n loadProgress(): SessionProgress | null;\n saveInput(questionId: string, value: string | string[]): void;\n loadInput(questionId: string): string | string[] | null;\n /** Forgets a saved input, such as a draft that has been submitted. */\n clearInput(questionId: string): void;\n clearAll(): void;\n /**\n * Called when the form is finished. Clears the saved session like `clearAll()` and also records\n * that it was completed, so another tab that is still open can tell \"finished\" from \"cleared\".\n * A store without it is cleared with `clearAll()`.\n */\n complete?(): void;\n /** When `complete()` was last called, in any tab (milliseconds since the epoch), or null. */\n completedAt?(): number | null;\n /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */\n dispose?(): void;\n}\n\nexport class SessionManager implements SessionStore {\n private readonly sessionId: string;\n private readonly available: boolean;\n\n constructor(private readonly storage: StorageBackend = globalThis.localStorage) {\n this.available = this.checkStorage();\n this.sessionId = this.loadOrCreateSessionId();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n if (!this.available) return;\n this.storage.setItem(keyProgress(this.sessionId), JSON.stringify({ currentPageId, answers }));\n }\n\n loadProgress(): SessionProgress | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyProgress(this.sessionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as SessionProgress;\n } catch {\n return null;\n }\n }\n\n saveInput(questionId: string, value: string | string[]): void {\n if (!this.available) return;\n this.storage.setItem(keyInput(this.sessionId, questionId), JSON.stringify(value));\n }\n\n loadInput(questionId: string): string | string[] | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyInput(this.sessionId, questionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as string | string[];\n } catch {\n return null;\n }\n }\n\n clearInput(questionId: string): void {\n if (!this.available) return;\n this.storage.removeItem(keyInput(this.sessionId, questionId));\n }\n\n complete(): void {\n this.clearAll();\n if (!this.available) return;\n this.storage.setItem(keyCompleted(this.sessionId), String(Date.now()));\n }\n\n completedAt(): number | null {\n if (!this.available) return null;\n const completedAt = Number(this.storage.getItem(keyCompleted(this.sessionId)));\n return Number.isFinite(completedAt) && completedAt > 0 ? completedAt : null;\n }\n\n clearAll(): void {\n if (!this.available) return;\n const progressKey = keyProgress(this.sessionId);\n const inputPrefix = `car:input:${this.sessionId}:`;\n const keysToRemove: string[] = [progressKey, keyCompleted(this.sessionId)];\n\n for (let i = 0; i < this.storage.length; i++) {\n const k = this.storage.key(i);\n if (k?.startsWith(inputPrefix)) keysToRemove.push(k);\n }\n\n for (const k of keysToRemove) {\n this.storage.removeItem(k);\n }\n }\n\n private loadOrCreateSessionId(): string {\n if (!this.available) return crypto.randomUUID();\n const existing = this.storage.getItem(KEY_SESSION);\n if (existing) return existing;\n const id = crypto.randomUUID();\n this.storage.setItem(KEY_SESSION, id);\n return id;\n }\n\n private checkStorage(): boolean {\n try {\n const probe = '__car_probe__';\n this.storage.setItem(probe, '1');\n this.storage.removeItem(probe);\n return true;\n } catch {\n return false;\n }\n }\n}\n","// packages/core/src/chat-controller.ts\n\nimport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nimport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nimport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nimport { RuleEngine } from './rule-engine.js';\nimport { parseSchema } from './schema-parser.js';\nimport { SessionManager, type SessionStore } from './session-manager.js';\nimport type {\n AnswerMap,\n ChatControllerConfig,\n ChatError,\n ChatEvent,\n ChatOptions,\n ChatPage,\n ChatQuestion,\n ChatSchema,\n ErrorLogResult,\n RouteEntry,\n SubmitOutcome,\n} from './types.js';\n\ntype EventName = 'page:submit' | 'form:complete';\ntype Listener = (event: ChatEvent) => void;\n\nconst DEFAULT_OPTIONS: ChatOptions = {\n userResumable: true,\n localStorage: true,\n autoComplete: true,\n allowEdit: false,\n autoHideQuestions: 0,\n autoHideAnswers: 0,\n hideAnimation: 'fade',\n typingSpeed: 0,\n thinkingDelay: 0,\n thinkingText: '',\n showContinueIcon: true,\n};\n\n/** Copies an object without its undefined values, so they cannot hide a default. */\nfunction stripUndefined<T extends object>(source: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(source).filter(([, value]) => value !== undefined),\n ) as Partial<T>;\n}\n\n/**\n * Central orchestrator for the chat-and-react form flow.\n * Drives page navigation, branching, session persistence, and event emission.\n */\nexport class ChatController {\n private schema: ChatSchema | null = null;\n private options: ChatOptions = { ...DEFAULT_OPTIONS };\n private readonly config: Required<ChatControllerConfig>;\n private readonly session: SessionStore;\n private readonly ruleEngine = new RuleEngine();\n private currentPage: ChatPage | null = null;\n private allAnswers: AnswerMap = {};\n private visitIndex = 0;\n /** Pages submitted in this session, in visit order. Used for editing earlier answers. */\n private route: RouteEntry[] = [];\n /** Index in `route` of the page being edited, or null when not editing. */\n private editIndex: number | null = null;\n /** The page the user was on before they started editing an earlier one. */\n private resumePageId: string | null = null;\n private readonly listeners = new Map<EventName, Set<Listener>>();\n\n /**\n * @param config - Controller configuration including schema and optional adapters.\n * @param sessionManager - Optional session manager for test injection.\n */\n constructor(config: ChatControllerConfig, sessionManager?: SessionStore) {\n this.config = {\n inputAdapter: config.inputAdapter ?? new IdentityInputAdapter(),\n outputAdapter: config.outputAdapter ?? new BrowserEventAdapter(),\n errorAdapter: config.errorAdapter ?? new ConsoleErrorAdapter(),\n schema: config.schema,\n options: config.options ?? {},\n };\n this.session = sessionManager ?? new SessionManager();\n }\n\n /**\n * Initialises the controller: parses the schema, resolves options,\n * restores any saved session, and positions to the first page.\n */\n start(): void {\n let raw: unknown;\n try {\n raw = this.config.inputAdapter.transform(this.config.schema);\n } catch (cause) {\n this.handleFatal({ code: 'ADAPTER_ERROR', message: 'InputAdapter.transform() threw', cause });\n }\n\n let schema: ChatSchema;\n try {\n schema = parseSchema(raw);\n } catch (e) {\n this.handleFatal(e as ChatError);\n }\n this.schema = schema;\n\n if (!this.session.isStorageAvailable) {\n this.logError({\n code: 'STORAGE_UNAVAILABLE',\n message: 'localStorage unavailable; running in memory-only mode',\n });\n }\n\n // Defaults, then the schema, then overrides passed in by the host (for\n // example element attributes).\n const schemaOpts = schema['x-chat-options'] ?? {};\n this.options = {\n ...DEFAULT_OPTIONS,\n ...stripUndefined(schemaOpts),\n ...stripUndefined(this.config.options),\n };\n\n if (this.options.userResumable) {\n const saved = this.session.loadProgress();\n if (saved) {\n const page = schema['x-chat-pages'].find((p) => p.id === saved.currentPageId);\n if (page) {\n this.allAnswers = saved.answers;\n this.currentPage = page;\n return;\n }\n }\n }\n\n this.currentPage = schema['x-chat-pages'][0];\n }\n\n /**\n * Returns the page currently being presented to the user, or null if complete.\n */\n getCurrentPage(): ChatPage | null {\n return this.currentPage;\n }\n\n /**\n * Returns the subset of questions on the current page that pass their\n * `x-chat-condition` rule (if any). Questions without a condition are always visible.\n */\n getVisibleQuestions(): ChatQuestion[] {\n if (!this.currentPage) return [];\n return this.currentPage.questions.filter((q) => this.isQuestionVisible(q));\n }\n\n /**\n * Returns the resolved options for the current session (schema options merged with defaults).\n */\n getOptions(): ChatOptions {\n return { ...this.options };\n }\n\n /**\n * Returns the pages the user has submitted in this session, in visit order.\n * A session restored from storage starts with an empty route.\n */\n getRoute(): RouteEntry[] {\n return this.route.map((entry) => ({ pageId: entry.pageId, answers: { ...entry.answers } }));\n }\n\n /**\n * Returns every answer given so far, for pre-filling inputs.\n */\n getAnswers(): AnswerMap {\n return { ...this.allAnswers };\n }\n\n /**\n * Re-opens a page the user already submitted so its answers can be changed.\n * Call `submitPage` afterwards to save the new answers. Not available once the\n * form is complete.\n *\n * @param routeIndex - Position of the page in `getRoute()`.\n * @returns true when the page was re-opened.\n */\n reopenPage(routeIndex: number): boolean {\n if (!this.schema || !this.currentPage) return false;\n const entry = this.route[routeIndex];\n if (!entry) return false;\n const page = this.schema['x-chat-pages'].find((p) => p.id === entry.pageId);\n if (!page) return false;\n\n // Remember where the user was, but only the first time: editing another\n // earlier page before finishing this edit must still return to that page.\n if (this.editIndex === null) this.resumePageId = this.currentPage.id;\n this.editIndex = routeIndex;\n this.currentPage = page;\n return true;\n }\n\n /**\n * Submits the answers for the current page and advances the session.\n * - Accumulates answers into `allAnswers`.\n * - Emits `page:submit` (suppressed when `userResumable` is false).\n * - Resolves branching to find the next page.\n * - Emits `form:complete` and clears session when `**FORM_COMPLETED**` is reached.\n *\n * When the page was re-opened with `reopenPage`, the new answers replace the\n * old ones and `page:submit` is emitted with `edited: true`. If the page's\n * branching still leads to the same next page, the form returns to the page the\n * user was on. If it leads somewhere else, the pages that followed it are\n * dropped, along with their answers, and the form continues from the new page.\n *\n * @param pageAnswers - Answers collected for the current page.\n */\n submitPage(pageAnswers: AnswerMap): SubmitOutcome {\n if (!this.currentPage || !this.schema) return 'advanced';\n\n const pageId = this.currentPage.id;\n const currentVisitIndex = this.visitIndex;\n const editedIndex = this.editIndex;\n const isEdit = editedIndex !== null;\n\n let outcome: SubmitOutcome = 'advanced';\n let nextPageId: string;\n let resumePageId: string | null = null;\n\n if (isEdit) {\n const entry = this.route[editedIndex];\n // Replace the old answers for this page, including any question that is\n // no longer visible after the change.\n const remaining = { ...this.allAnswers };\n for (const questionId of Object.keys(entry.answers)) delete remaining[questionId];\n this.allAnswers = { ...remaining, ...pageAnswers };\n entry.answers = { ...pageAnswers };\n\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n const expectedNextPageId = this.route[editedIndex + 1]?.pageId ?? this.resumePageId;\n\n if (nextPageId === expectedNextPageId) {\n outcome = 'edit-resumed';\n resumePageId = this.resumePageId;\n } else {\n outcome = 'edit-rerouted';\n // The route changed: forget the pages that followed and their answers.\n for (const later of this.route.slice(editedIndex + 1)) {\n for (const questionId of Object.keys(later.answers)) delete this.allAnswers[questionId];\n }\n this.route.length = editedIndex + 1;\n }\n this.editIndex = null;\n this.resumePageId = null;\n } else {\n this.allAnswers = { ...this.allAnswers, ...pageAnswers };\n this.route.push({ pageId, answers: { ...pageAnswers } });\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n }\n\n if (this.options.userResumable) {\n const progressPageId =\n outcome === 'edit-resumed' && resumePageId\n ? resumePageId\n : nextPageId === '**FORM_COMPLETED**'\n ? pageId\n : nextPageId;\n this.session.saveProgress(progressPageId, this.allAnswers);\n\n const submitEvent: ChatEvent = {\n type: 'page:submit',\n sessionId: this.session.getSessionId(),\n pageId,\n visitIndex: currentVisitIndex,\n answers: pageAnswers,\n ...(isEdit ? { edited: true } : {}),\n };\n this.safeEmit(submitEvent);\n this.emit('page:submit', submitEvent);\n }\n\n this.visitIndex++;\n\n if (outcome === 'edit-resumed' && resumePageId) {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === resumePageId) ?? null;\n return outcome;\n }\n\n if (nextPageId === '**FORM_COMPLETED**') {\n if (this.session.complete) {\n this.session.complete();\n } else {\n this.session.clearAll();\n }\n const completeEvent: ChatEvent = {\n type: 'form:complete',\n sessionId: this.session.getSessionId(),\n allAnswers: this.allAnswers,\n };\n this.safeEmit(completeEvent);\n this.emit('form:complete', completeEvent);\n this.currentPage = null;\n } else {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === nextPageId) ?? null;\n }\n return outcome;\n }\n\n /**\n * Registers an event listener for the given event type.\n *\n * @param event - The event to listen for.\n * @param listener - The callback to invoke when the event fires.\n */\n on(event: EventName, listener: Listener): void {\n let listenersForEvent = this.listeners.get(event);\n if (!listenersForEvent) {\n listenersForEvent = new Set();\n this.listeners.set(event, listenersForEvent);\n }\n listenersForEvent.add(listener);\n }\n\n /**\n * Removes a previously registered event listener.\n *\n * @param event - The event type to deregister from.\n * @param listener - The specific listener to remove.\n */\n off(event: EventName, listener: Listener): void {\n this.listeners.get(event)?.delete(listener);\n }\n\n /**\n * Tears down the controller: removes all listeners and nulls internal state.\n */\n destroy(): void {\n this.session.dispose?.();\n this.listeners.clear();\n this.currentPage = null;\n this.schema = null;\n }\n\n /**\n * Evaluates whether a question should be shown based on its `x-chat-condition`.\n * Returns true when no condition is present. On evaluation failure, logs and returns false.\n */\n private isQuestionVisible(question: ChatQuestion): boolean {\n const condition = question['x-chat-condition'];\n if (!condition) return true;\n\n const rules = this.schema?.['x-chat-rules'] ?? {};\n let rule: Record<string, unknown>;\n\n if ('ref' in condition && typeof (condition as { ref?: unknown }).ref === 'string') {\n const ref = (condition as { ref: string }).ref;\n rule = rules[ref];\n if (!rule) return false;\n } else {\n rule = condition as Record<string, unknown>;\n }\n\n try {\n return this.ruleEngine.evaluate(rule, this.answersForRules());\n } catch (e) {\n this.logError(e as ChatError);\n return false;\n }\n }\n\n /**\n * The answers question conditions are checked against. While an earlier page is\n * being edited, that page's own answers are left out, so its questions are\n * shown exactly as they were the first time the page was asked.\n */\n private answersForRules(): AnswerMap {\n if (this.editIndex === null) return this.allAnswers;\n const rest = { ...this.allAnswers };\n for (const questionId of Object.keys(this.route[this.editIndex].answers))\n delete rest[questionId];\n return rest;\n }\n\n /**\n * Walks the branching rules top-to-bottom. Returns the first match's destination_id.\n * Falls back to `**FORM_COMPLETED**` if nothing matches (should not happen with valid schemas).\n */\n private resolveBranching(page: ChatPage, answers: AnswerMap): string {\n for (const rule of page.branching) {\n if (rule.type === 'always') return rule.destination_id;\n if (rule.type === 'if' && answers[rule.form_element] === rule.value) {\n return rule.destination_id;\n }\n }\n return '**FORM_COMPLETED**';\n }\n\n /**\n * Calls `outputAdapter.emit()` and swallows adapter errors (logging them instead).\n */\n private safeEmit(event: ChatEvent): void {\n try {\n this.config.outputAdapter.emit(event);\n } catch (cause) {\n this.logError({\n code: 'ADAPTER_ERROR',\n message: `OutputAdapter.emit() threw for ${event.type}`,\n cause,\n });\n }\n }\n\n /**\n * Notifies all registered listeners for the given event.\n */\n private emit(event: EventName, payload: ChatEvent): void {\n this.listeners.get(event)?.forEach((l) => {\n l(payload);\n });\n }\n\n /**\n * Logs the error via the error adapter and then throws it.\n * Used for unrecoverable errors (schema invalid, adapter failure on start).\n */\n private handleFatal(error: ChatError): never {\n this.logError(error);\n throw error;\n }\n\n /**\n * Sends the error to the error adapter without waiting for it. If the adapter\n * reports a failure, rejects, or throws, that is written to `console.error` so\n * the original error is never lost and the form keeps running.\n */\n private logError(entry: ChatError): void {\n const reportFailure = (reason: unknown): void => {\n console.error('[chat-and-react] ErrorLogAdapter failed to record an error:', reason, entry);\n };\n try {\n // `Promise.resolve` also tolerates older custom adapters that return nothing.\n void Promise.resolve(this.config.errorAdapter.log(entry)).then(\n (result: ErrorLogResult | undefined): void => {\n if (result?.ok === false) {\n reportFailure(result.error);\n }\n },\n reportFailure,\n );\n } catch (cause) {\n reportFailure(cause);\n }\n }\n}\n","import type {\n StorageAdapter,\n StoredValue,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\nimport type { SessionProgress, SessionStore } from '../session-manager.js';\nimport type { AnswerMap, ChatError } from '../types.js';\n\nconst KEY_SESSION = 'car:session-id';\nconst keyRecord = (sessionId: string): string => `car:session:${sessionId}`;\n\n/** After this many revision conflicts in a row, a save is abandoned and reported. */\nconst MAX_CONFLICT_RETRIES = 3;\n\ntype InputValue = string | string[];\n\n/** Everything saved for one session, stored under a single key. */\ninterface SessionRecord {\n progress: SessionProgress | null;\n inputs: Record<string, InputValue>;\n /** When the form was finished (milliseconds since the epoch), if it has been. */\n completedAt?: number;\n}\n\nexport interface AdapterSessionManagerOptions {\n /** Called when the adapter fails. The session keeps working from memory. */\n onError?: (error: ChatError) => void;\n /**\n * Called when saved progress changes because of a write made elsewhere, such as\n * another tab. `null` means the session was cleared there.\n */\n onExternalChange?: (progress: SessionProgress | null) => void;\n}\n\nconst emptyRecord = (): SessionRecord => ({ progress: null, inputs: {} });\n\nfunction parseRecord(raw: string): SessionRecord | null {\n try {\n const parsed = JSON.parse(raw) as Partial<SessionRecord>;\n return {\n progress: parsed.progress ?? null,\n inputs: parsed.inputs && typeof parsed.inputs === 'object' ? parsed.inputs : {},\n ...(typeof parsed.completedAt === 'number' ? { completedAt: parsed.completedAt } : {}),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * A session store backed by a `StorageAdapter`. `ChatController` is synchronous, so\n * the saved session is loaded into memory by `create()`, reads come from memory, and\n * writes are saved in the background. Failures are reported through `onError`.\n *\n * Saves carry the revision they were based on. If another tab wrote first, the two\n * are merged: inputs per question, and progress only when this tab saved progress\n * itself, so a tab that only typed cannot overwrite another tab's progress.\n */\nexport class AdapterSessionManager implements SessionStore {\n private record: SessionRecord;\n private revision: string | undefined;\n private readonly dirtyInputs = new Set<string>();\n private dirtyProgress = false;\n /** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */\n private resetPending = false;\n private pending = false;\n private persisting: Promise<void> | null = null;\n private readonly signal: AbortSignal = new AbortController().signal;\n private unsubscribe: (() => void) | null = null;\n\n private constructor(\n private readonly adapter: StorageAdapter,\n private readonly sessionId: string,\n private readonly available: boolean,\n initial: StoredValue | null,\n private readonly options: AdapterSessionManagerOptions,\n ) {\n const parsed: SessionRecord | null = initial ? parseRecord(initial.value) : null;\n this.record = parsed ?? emptyRecord();\n this.revision = initial?.revision;\n if (initial && !parsed) {\n this.report(\n `Saved session ${sessionId} could not be read as JSON; starting with an empty session`,\n );\n }\n if (available) {\n this.unsubscribe =\n adapter.subscribe?.(keyRecord(sessionId), (stored) => this.applyRemote(stored)) ?? null;\n }\n }\n\n /** Loads (or creates) the session id and its saved record, then returns the manager. */\n static async create(\n adapter: StorageAdapter,\n options: AdapterSessionManagerOptions = {},\n ): Promise<AdapterSessionManager> {\n const signal: AbortSignal = new AbortController().signal;\n const fail = (message: string): void =>\n options.onError?.({ code: 'STORAGE_UNAVAILABLE', message });\n\n let sessionId: string | null = null;\n let available = true;\n\n const idResult: StoreResult<StoredValue | null> = await adapter.get(KEY_SESSION, { signal });\n if (idResult.ok) {\n sessionId = idResult.value?.value ?? null;\n } else {\n available = false;\n fail(`Could not read the session id: ${idResult.error}. Running in memory-only mode`);\n }\n if (available && sessionId === null) {\n sessionId = crypto.randomUUID();\n const saved: StoreResult<StoredValue> = await adapter.set(KEY_SESSION, sessionId, {\n signal,\n });\n if (!saved.ok) {\n available = false;\n fail(`Could not save the session id: ${saved.error}. Running in memory-only mode`);\n }\n }\n const id: string = sessionId ?? crypto.randomUUID();\n\n let initial: StoredValue | null = null;\n if (available) {\n const recordResult: StoreResult<StoredValue | null> = await adapter.get(keyRecord(id), {\n signal,\n });\n if (recordResult.ok) {\n initial = recordResult.value;\n } else {\n available = false;\n fail(`Could not read saved progress: ${recordResult.error}. Running in memory-only mode`);\n }\n }\n return new AdapterSessionManager(adapter, id, available, initial, options);\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n this.record.progress = { currentPageId, answers };\n this.dirtyProgress = true;\n this.schedulePersist();\n }\n\n loadProgress(): SessionProgress | null {\n return this.record.progress;\n }\n\n saveInput(questionId: string, value: InputValue): void {\n this.record.inputs[questionId] = value;\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n loadInput(questionId: string): InputValue | null {\n return this.record.inputs[questionId] ?? null;\n }\n\n clearInput(questionId: string): void {\n if (!(questionId in this.record.inputs)) return;\n delete this.record.inputs[questionId];\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n complete(): void {\n this.record = { progress: null, inputs: {}, completedAt: Date.now() };\n this.dirtyInputs.clear();\n this.dirtyProgress = true;\n this.resetPending = true;\n this.schedulePersist();\n }\n\n completedAt(): number | null {\n return this.record.completedAt ?? null;\n }\n\n clearAll(): void {\n this.record = emptyRecord();\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.resetPending = false;\n this.pending = false;\n if (!this.available) return;\n void this.removeRecord();\n }\n\n dispose(): void {\n this.unsubscribe?.();\n this.unsubscribe = null;\n }\n\n /** Resolves when every save made so far has finished. Mostly for tests. */\n async flush(): Promise<void> {\n while (this.persisting) {\n await this.persisting;\n }\n }\n\n private report(message: string, cause?: unknown): void {\n this.options.onError?.({ code: 'STORAGE_UNAVAILABLE', message, cause });\n }\n\n private schedulePersist(): void {\n if (!this.available) return;\n this.pending = true;\n this.persisting ??= this.drain();\n }\n\n /** Saves until nothing is left to save. Never rejects. */\n private async drain(): Promise<void> {\n let conflicts = 0;\n try {\n while (this.pending) {\n this.pending = false;\n const outcome: 'saved' | 'conflict' | 'failed' = await this.persistOnce();\n conflicts = outcome === 'conflict' ? conflicts + 1 : 0;\n if (outcome === 'conflict') {\n if (conflicts >= MAX_CONFLICT_RETRIES) {\n this.report(\n `Gave up saving session ${this.sessionId} after ${conflicts} revision conflicts in a row`,\n );\n break;\n }\n this.pending = true;\n }\n }\n } finally {\n // Cleared in the same tick the loop ends, so a save requested right now starts a new drain.\n this.persisting = null;\n }\n }\n\n private async persistOnce(): Promise<'saved' | 'conflict' | 'failed'> {\n const sentInputs = new Set<string>(this.dirtyInputs);\n const sentProgress: boolean = this.dirtyProgress;\n const sentReset: boolean = this.resetPending;\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.resetPending = false;\n\n const result: StoreResult<StoredValue> = await this.adapter.set(\n keyRecord(this.sessionId),\n JSON.stringify(this.record),\n { signal: this.signal, ifRevision: this.revision },\n );\n if (result.ok) {\n this.revision = result.value.revision;\n return 'saved';\n }\n\n // Keep what was not saved so the next attempt includes it.\n for (const questionId of sentInputs) this.dirtyInputs.add(questionId);\n this.dirtyProgress = this.dirtyProgress || sentProgress;\n this.resetPending = this.resetPending || sentReset;\n\n if (result.code !== 'conflict') {\n this.report(`Could not save session ${this.sessionId}: ${result.error}`);\n return 'failed';\n }\n const latest: StoreResult<StoredValue | null> = await this.adapter.get(\n keyRecord(this.sessionId),\n { signal: this.signal },\n );\n if (!latest.ok) {\n this.report(\n `Session ${this.sessionId} changed elsewhere, but the new version could not be read: ${latest.error}`,\n );\n return 'failed';\n }\n this.applyRemote(latest.value);\n return 'conflict';\n }\n\n /** Merges a version written elsewhere with this tab's unsaved changes. */\n private applyRemote(stored: StoredValue | null): void {\n const before: string = JSON.stringify(this.record.progress);\n if (!stored) {\n if (!this.dirtyProgress && this.dirtyInputs.size === 0) {\n this.record = emptyRecord();\n }\n this.revision = undefined;\n } else {\n const remote: SessionRecord | null = parseRecord(stored.value);\n if (!remote) {\n this.report(\n `Session ${this.sessionId} was changed elsewhere to a value that is not valid JSON; ignoring it`,\n );\n this.revision = stored.revision;\n return;\n }\n // A finished form leaves nothing of another tab's behind; otherwise the two are merged.\n const inputs: Record<string, InputValue> = this.resetPending ? {} : { ...remote.inputs };\n for (const questionId of this.dirtyInputs) {\n const mine: InputValue | undefined = this.record.inputs[questionId];\n // An input that is dirty but missing here was cleared on this tab.\n if (mine !== undefined) {\n inputs[questionId] = mine;\n } else {\n delete inputs[questionId];\n }\n }\n const completedAt: number | undefined =\n Math.max(this.record.completedAt ?? 0, remote.completedAt ?? 0) || undefined;\n this.record = {\n progress: this.dirtyProgress ? this.record.progress : remote.progress,\n inputs,\n ...(completedAt !== undefined ? { completedAt } : {}),\n };\n this.revision = stored.revision;\n }\n if (JSON.stringify(this.record.progress) !== before) {\n this.options.onExternalChange?.(this.record.progress);\n }\n }\n\n private async removeRecord(): Promise<void> {\n const key: string = keyRecord(this.sessionId);\n const result: StoreResult<unknown> = this.adapter.remove\n ? await this.adapter.remove(key, { signal: this.signal })\n : await this.adapter.set(key, JSON.stringify(emptyRecord()), { signal: this.signal });\n if (result.ok) {\n this.revision = undefined;\n } else {\n this.report(`Could not clear session ${this.sessionId}: ${result.error}`);\n }\n }\n}\n"],"mappings":"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","names":["KEY_SESSION"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@richardmcquiston01/chat-and-react-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Headless controller, JSON Schema validation, and session management for chat-and-react — a framework-agnostic conversational form library.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Richard McQuiston <richard.mcquiston01@gmail.com>",
|