@richardmcquiston01/chat-and-react-core 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +41 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -19
- package/dist/index.d.ts +16 -19
- package/dist/index.mjs +40 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ controller.submitPage({ question_id: "answer value" });
|
|
|
41
41
|
- `SessionManager` — session ID and progress persistence with an injectable `StorageBackend`
|
|
42
42
|
- `ChatController` — page-graph navigation, answer accumulation, event emission (`page:submit`, `form:complete`)
|
|
43
43
|
- Built-in adapters: `IdentityInputAdapter`, `BrowserEventAdapter`, `ConsoleErrorAdapter`
|
|
44
|
-
- `AdapterSessionManager` — saves progress through any storage adapter; `localStorageAdapter`, `sessionStorageAdapter` and `indexedDbAdapter`
|
|
44
|
+
- `AdapterSessionManager` — saves progress through any storage adapter; the `localStorageAdapter`, `sessionStorageAdapter` and `indexedDbAdapter` it takes come from `@richardmcquiston01/storage-adapters`
|
|
45
45
|
|
|
46
46
|
## Full documentation
|
|
47
47
|
|
package/dist/index.cjs
CHANGED
|
@@ -35,16 +35,10 @@ __export(index_exports, {
|
|
|
35
35
|
ChatController: () => ChatController,
|
|
36
36
|
ConsoleErrorAdapter: () => ConsoleErrorAdapter,
|
|
37
37
|
IdentityInputAdapter: () => IdentityInputAdapter,
|
|
38
|
-
RuleEngine: () => RuleEngine,
|
|
39
38
|
SessionManager: () => SessionManager,
|
|
40
|
-
|
|
41
|
-
localStorageAdapter: () => import_browser.localStorageAdapter,
|
|
42
|
-
parseSchema: () => parseSchema,
|
|
43
|
-
sessionStorageAdapter: () => import_browser.sessionStorageAdapter
|
|
39
|
+
parseSchema: () => parseSchema
|
|
44
40
|
});
|
|
45
41
|
module.exports = __toCommonJS(index_exports);
|
|
46
|
-
var import_browser = require("@richardmcquiston01/storage-adapters/browser");
|
|
47
|
-
var import_indexeddb = require("@richardmcquiston01/storage-adapters/indexeddb");
|
|
48
42
|
|
|
49
43
|
// src/adapters/browser-event-adapter.ts
|
|
50
44
|
var BrowserEventAdapter = class {
|
|
@@ -191,6 +185,7 @@ function makeError(message) {
|
|
|
191
185
|
var KEY_SESSION = "car:session-id";
|
|
192
186
|
var keyProgress = (id) => `car:progress:${id}`;
|
|
193
187
|
var keyInput = (sessionId, questionId) => `car:input:${sessionId}:${questionId}`;
|
|
188
|
+
var keyCompleted = (sessionId) => `car:completed:${sessionId}`;
|
|
194
189
|
var SessionManager = class {
|
|
195
190
|
constructor(storage = globalThis.localStorage) {
|
|
196
191
|
this.storage = storage;
|
|
@@ -235,11 +230,21 @@ var SessionManager = class {
|
|
|
235
230
|
if (!this.available) return;
|
|
236
231
|
this.storage.removeItem(keyInput(this.sessionId, questionId));
|
|
237
232
|
}
|
|
233
|
+
complete() {
|
|
234
|
+
this.clearAll();
|
|
235
|
+
if (!this.available) return;
|
|
236
|
+
this.storage.setItem(keyCompleted(this.sessionId), String(Date.now()));
|
|
237
|
+
}
|
|
238
|
+
completedAt() {
|
|
239
|
+
if (!this.available) return null;
|
|
240
|
+
const completedAt = Number(this.storage.getItem(keyCompleted(this.sessionId)));
|
|
241
|
+
return Number.isFinite(completedAt) && completedAt > 0 ? completedAt : null;
|
|
242
|
+
}
|
|
238
243
|
clearAll() {
|
|
239
244
|
if (!this.available) return;
|
|
240
245
|
const progressKey = keyProgress(this.sessionId);
|
|
241
246
|
const inputPrefix = `car:input:${this.sessionId}:`;
|
|
242
|
-
const keysToRemove = [progressKey];
|
|
247
|
+
const keysToRemove = [progressKey, keyCompleted(this.sessionId)];
|
|
243
248
|
for (let i = 0; i < this.storage.length; i++) {
|
|
244
249
|
const k = this.storage.key(i);
|
|
245
250
|
if (k?.startsWith(inputPrefix)) keysToRemove.push(k);
|
|
@@ -479,7 +484,11 @@ var ChatController = class {
|
|
|
479
484
|
return outcome;
|
|
480
485
|
}
|
|
481
486
|
if (nextPageId === "**FORM_COMPLETED**") {
|
|
482
|
-
this.session.
|
|
487
|
+
if (this.session.complete) {
|
|
488
|
+
this.session.complete();
|
|
489
|
+
} else {
|
|
490
|
+
this.session.clearAll();
|
|
491
|
+
}
|
|
483
492
|
const completeEvent = {
|
|
484
493
|
type: "form:complete",
|
|
485
494
|
sessionId: this.session.getSessionId(),
|
|
@@ -637,7 +646,8 @@ function parseRecord(raw) {
|
|
|
637
646
|
const parsed = JSON.parse(raw);
|
|
638
647
|
return {
|
|
639
648
|
progress: parsed.progress ?? null,
|
|
640
|
-
inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {}
|
|
649
|
+
inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {},
|
|
650
|
+
...typeof parsed.completedAt === "number" ? { completedAt: parsed.completedAt } : {}
|
|
641
651
|
};
|
|
642
652
|
} catch {
|
|
643
653
|
return null;
|
|
@@ -651,6 +661,8 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
651
661
|
this.options = options;
|
|
652
662
|
this.dirtyInputs = /* @__PURE__ */ new Set();
|
|
653
663
|
this.dirtyProgress = false;
|
|
664
|
+
/** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
|
|
665
|
+
this.resetPending = false;
|
|
654
666
|
this.pending = false;
|
|
655
667
|
this.persisting = null;
|
|
656
668
|
this.signal = new AbortController().signal;
|
|
@@ -733,10 +745,21 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
733
745
|
this.dirtyInputs.add(questionId);
|
|
734
746
|
this.schedulePersist();
|
|
735
747
|
}
|
|
748
|
+
complete() {
|
|
749
|
+
this.record = { progress: null, inputs: {}, completedAt: Date.now() };
|
|
750
|
+
this.dirtyInputs.clear();
|
|
751
|
+
this.dirtyProgress = true;
|
|
752
|
+
this.resetPending = true;
|
|
753
|
+
this.schedulePersist();
|
|
754
|
+
}
|
|
755
|
+
completedAt() {
|
|
756
|
+
return this.record.completedAt ?? null;
|
|
757
|
+
}
|
|
736
758
|
clearAll() {
|
|
737
759
|
this.record = emptyRecord();
|
|
738
760
|
this.dirtyInputs.clear();
|
|
739
761
|
this.dirtyProgress = false;
|
|
762
|
+
this.resetPending = false;
|
|
740
763
|
this.pending = false;
|
|
741
764
|
if (!this.available) return;
|
|
742
765
|
void this.removeRecord();
|
|
@@ -784,8 +807,10 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
784
807
|
async persistOnce() {
|
|
785
808
|
const sentInputs = new Set(this.dirtyInputs);
|
|
786
809
|
const sentProgress = this.dirtyProgress;
|
|
810
|
+
const sentReset = this.resetPending;
|
|
787
811
|
this.dirtyInputs.clear();
|
|
788
812
|
this.dirtyProgress = false;
|
|
813
|
+
this.resetPending = false;
|
|
789
814
|
const result = await this.adapter.set(
|
|
790
815
|
keyRecord(this.sessionId),
|
|
791
816
|
JSON.stringify(this.record),
|
|
@@ -797,6 +822,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
797
822
|
}
|
|
798
823
|
for (const questionId of sentInputs) this.dirtyInputs.add(questionId);
|
|
799
824
|
this.dirtyProgress = this.dirtyProgress || sentProgress;
|
|
825
|
+
this.resetPending = this.resetPending || sentReset;
|
|
800
826
|
if (result.code !== "conflict") {
|
|
801
827
|
this.report(`Could not save session ${this.sessionId}: ${result.error}`);
|
|
802
828
|
return "failed";
|
|
@@ -831,7 +857,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
831
857
|
this.revision = stored.revision;
|
|
832
858
|
return;
|
|
833
859
|
}
|
|
834
|
-
const inputs = { ...remote.inputs };
|
|
860
|
+
const inputs = this.resetPending ? {} : { ...remote.inputs };
|
|
835
861
|
for (const questionId of this.dirtyInputs) {
|
|
836
862
|
const mine = this.record.inputs[questionId];
|
|
837
863
|
if (mine !== void 0) {
|
|
@@ -840,9 +866,11 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
840
866
|
delete inputs[questionId];
|
|
841
867
|
}
|
|
842
868
|
}
|
|
869
|
+
const completedAt = Math.max(this.record.completedAt ?? 0, remote.completedAt ?? 0) || void 0;
|
|
843
870
|
this.record = {
|
|
844
871
|
progress: this.dirtyProgress ? this.record.progress : remote.progress,
|
|
845
|
-
inputs
|
|
872
|
+
inputs,
|
|
873
|
+
...completedAt !== void 0 ? { completedAt } : {}
|
|
846
874
|
};
|
|
847
875
|
this.revision = stored.revision;
|
|
848
876
|
}
|
|
@@ -867,11 +895,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
867
895
|
ChatController,
|
|
868
896
|
ConsoleErrorAdapter,
|
|
869
897
|
IdentityInputAdapter,
|
|
870
|
-
RuleEngine,
|
|
871
898
|
SessionManager,
|
|
872
|
-
|
|
873
|
-
localStorageAdapter,
|
|
874
|
-
parseSchema,
|
|
875
|
-
sessionStorageAdapter
|
|
899
|
+
parseSchema
|
|
876
900
|
});
|
|
877
901
|
//# sourceMappingURL=index.cjs.map
|
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":"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","names":["BaseConsoleErrorAdapter","jsonLogic","KEY_SESSION"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/adapters/browser-event-adapter.ts","../src/adapters/console-error-adapter.ts","../src/adapters/identity-input-adapter.ts","../src/rule-engine.ts","../src/schema-parser.ts","../src/session-manager.ts","../src/chat-controller.ts","../src/storage/adapter-session-manager.ts"],"sourcesContent":["export type { StorageAdapter } from '@richardmcquiston01/storage-adapters';\nexport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nexport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nexport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nexport { ChatController } from './chat-controller.js';\nexport { parseSchema } from './schema-parser.js';\nexport type { SessionProgress, SessionStore } from './session-manager.js';\nexport { SessionManager } from './session-manager.js';\nexport type { AdapterSessionManagerOptions } from './storage/adapter-session-manager.js';\nexport { AdapterSessionManager } from './storage/adapter-session-manager.js';\nexport type {\n AnswerMap,\n BranchingAlways,\n BranchingIf,\n BranchingRule,\n ChatConditionRef,\n ChatControllerConfig,\n ChatError,\n ChatErrorCode,\n ChatEvent,\n ChatInputType,\n ChatOptions,\n ChatPage,\n ChatQuestion,\n ChatSchema,\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n FormCompleteEvent,\n HideAnimation,\n InputAdapter,\n JsonLogicRule,\n OutputAdapter,\n PageSubmitEvent,\n} from './types.js';\n","import type { ChatEvent, OutputAdapter } from '../types.js';\n\nexport class BrowserEventAdapter implements OutputAdapter {\n emit(event: ChatEvent): void {\n if (typeof document === 'undefined') return;\n const name = event.type === 'page:submit' ? 'chat-page-submit' : 'chat-form-complete';\n document.dispatchEvent(new CustomEvent(name, { detail: event, bubbles: true }));\n }\n}\n","import {\n ConsoleErrorAdapter as BaseConsoleErrorAdapter,\n type ConsoleErrorOptions,\n} from '@richardmcquiston01/error-log-adapter';\nimport type { ChatErrorCode } from '../types.js';\n\n/** Writes errors to `console.error`, prefixed with `[chat-and-react]` unless overridden. */\nexport class ConsoleErrorAdapter extends BaseConsoleErrorAdapter<ChatErrorCode> {\n constructor(options: ConsoleErrorOptions = {}) {\n super({ prefix: '[chat-and-react]', ...options });\n }\n}\n","import type { InputAdapter } from '../types.js';\n\nexport class IdentityInputAdapter implements InputAdapter {\n transform(raw: unknown): unknown {\n return raw;\n }\n}\n","import jsonLogic from 'json-logic-js';\nimport type { AnswerMap, JsonLogicRule } from './types.js';\n\n/**\n * Evaluates JSON Logic rules against provided answers.\n * Wraps json-logic-js to provide type-safe rule evaluation.\n */\nexport class RuleEngine {\n /**\n * Evaluates a JSON Logic rule against an answer map.\n * @param rule The JSON Logic rule to evaluate\n * @param answers The answer map to evaluate the rule against\n * @returns The boolean result of the rule evaluation\n * @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails\n */\n evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean {\n try {\n // json-logic-js apply() returns any; cast to boolean\n return Boolean(\n (jsonLogic as { apply: (rule: unknown, data: unknown) => unknown }).apply(rule, answers),\n );\n } catch (cause) {\n throw { code: 'RULE_EVAL_FAILED', message: 'Rule evaluation failed', cause };\n }\n }\n}\n","import type { ChatPage, ChatSchema } from './types.js';\n\nconst VALID_INPUTS = new Set(['text', 'textarea', 'dropdown', 'radio', 'checkbox']);\nconst FORM_COMPLETED = '**FORM_COMPLETED**';\n\n/**\n * Parses and validates an unknown input as a {@link ChatSchema}.\n *\n * @param raw - The raw input to validate.\n * @returns The typed {@link ChatSchema} if valid.\n * @throws `{ code: 'SCHEMA_INVALID', message: string }` on any validation failure.\n */\nexport function parseSchema(raw: unknown): ChatSchema {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Schema must be a non-null object');\n }\n\n const obj = raw as Record<string, unknown>;\n\n if (obj['x-chat-version'] !== '1') {\n throw makeError('x-chat-version must be \"1\"');\n }\n\n if (!Array.isArray(obj['x-chat-pages'])) {\n throw makeError('x-chat-pages must be an array');\n }\n\n const pages = obj['x-chat-pages'] as unknown[];\n const rules = (\n typeof obj['x-chat-rules'] === 'object' && obj['x-chat-rules'] !== null\n ? obj['x-chat-rules']\n : {}\n ) as Record<string, unknown>;\n\n const pageIds = new Set<string>();\n const questionIds = new Set<string>();\n\n for (const page of pages) {\n validatePage(page, pageIds, questionIds, rules);\n }\n\n // Second pass: validate branching destinations now that all page ids are known\n for (const page of pages) {\n const p = page as ChatPage;\n for (const rule of p.branching) {\n if (rule.destination_id !== FORM_COMPLETED && !pageIds.has(rule.destination_id)) {\n throw makeError(\n `branching destination_id \"${rule.destination_id}\" does not reference a known page id`,\n );\n }\n if (rule.type === 'if' && !questionIds.has(rule.form_element)) {\n throw makeError(\n `branching form_element \"${rule.form_element}\" does not reference a known question id`,\n );\n }\n }\n }\n\n return raw as ChatSchema;\n}\n\n/**\n * Validates a single page object, collecting its id and question ids.\n *\n * @param raw - The raw page value.\n * @param pageIds - Set of already-seen page ids (mutated in place).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validatePage(\n raw: unknown,\n pageIds: Set<string>,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each page must be a non-null object');\n }\n const page = raw as Record<string, unknown>;\n\n if (typeof page['id'] !== 'string' || !page['id']) {\n throw makeError('Page id must be a non-empty string');\n }\n if (pageIds.has(page['id'] as string)) {\n throw makeError(`Duplicate page id \"${page['id']}\"`);\n }\n pageIds.add(page['id'] as string);\n\n if (!Array.isArray(page['questions'])) {\n throw makeError(`Page \"${page['id']}\" must have a questions array`);\n }\n\n const branching = page['branching'];\n if (!Array.isArray(branching) || (branching as unknown[]).length === 0) {\n throw makeError(`Page \"${page['id']}\" must have a non-empty branching array`);\n }\n\n const lastRule = (branching as unknown[])[branching.length - 1] as Record<string, unknown>;\n if (lastRule['type'] !== 'always') {\n throw makeError(`Page \"${page['id']}\" branching must end with a rule of type \"always\"`);\n }\n\n for (const q of page['questions'] as unknown[]) {\n validateQuestion(q, page['id'] as string, questionIds, rules);\n }\n}\n\n/**\n * Validates a single question object, collecting its id.\n *\n * @param raw - The raw question value.\n * @param pageId - The id of the containing page (for error messages).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validateQuestion(\n raw: unknown,\n pageId: string,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each question must be a non-null object');\n }\n const q = raw as Record<string, unknown>;\n\n if (typeof q['id'] !== 'string' || !q['id']) {\n throw makeError(`In page \"${pageId}\": question id must be a non-empty string`);\n }\n if (questionIds.has(q['id'] as string)) {\n throw makeError(`Duplicate question id \"${q['id']}\"`);\n }\n questionIds.add(q['id'] as string);\n\n if (!VALID_INPUTS.has(q['x-chat-input'] as string)) {\n throw makeError(\n `Question \"${q['id']}\" has invalid x-chat-input \"${q['x-chat-input']}\". ` +\n `Must be one of: text, textarea, dropdown, radio, checkbox`,\n );\n }\n\n const condition = q['x-chat-condition'];\n if (condition && typeof condition === 'object' && !Array.isArray(condition)) {\n const c = condition as Record<string, unknown>;\n if ('ref' in c && typeof c['ref'] === 'string') {\n if (!(c['ref'] in rules)) {\n throw makeError(`Question \"${q['id']}\" x-chat-condition refs unknown rule \"${c['ref']}\"`);\n }\n }\n }\n}\n\n/**\n * Creates a schema validation error object.\n *\n * @param message - Human-readable description of the validation failure.\n * @returns An error object with `code: 'SCHEMA_INVALID'`.\n */\nfunction makeError(message: string): { code: 'SCHEMA_INVALID'; message: string } {\n return { code: 'SCHEMA_INVALID', message };\n}\n","// packages/core/src/session-manager.ts\nimport type { AnswerMap } from './types.js';\n\nexport interface StorageBackend {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n key(index: number): string | null;\n readonly length: number;\n}\n\nexport interface SessionProgress {\n currentPageId: string;\n answers: AnswerMap;\n}\n\nconst KEY_SESSION = 'car:session-id';\nconst keyProgress = (id: string) => `car:progress:${id}`;\nconst keyInput = (sessionId: string, questionId: string) => `car:input:${sessionId}:${questionId}`;\nconst keyCompleted = (sessionId: string) => `car:completed:${sessionId}`;\n\n/**\n * What `ChatController` needs from session storage. `SessionManager` keeps it in a\n * synchronous Web Storage object; `AdapterSessionManager` keeps it in a `StorageAdapter`.\n */\nexport interface SessionStore {\n readonly isStorageAvailable: boolean;\n getSessionId(): string;\n saveProgress(currentPageId: string, answers: AnswerMap): void;\n loadProgress(): SessionProgress | null;\n saveInput(questionId: string, value: string | string[]): void;\n loadInput(questionId: string): string | string[] | null;\n /** Forgets a saved input, such as a draft that has been submitted. */\n clearInput(questionId: string): void;\n clearAll(): void;\n /**\n * Called when the form is finished. Clears the saved session like `clearAll()` and also records\n * that it was completed, so another tab that is still open can tell \"finished\" from \"cleared\".\n * A store without it is cleared with `clearAll()`.\n */\n complete?(): void;\n /** When `complete()` was last called, in any tab (milliseconds since the epoch), or null. */\n completedAt?(): number | null;\n /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */\n dispose?(): void;\n}\n\nexport class SessionManager implements SessionStore {\n private readonly sessionId: string;\n private readonly available: boolean;\n\n constructor(private readonly storage: StorageBackend = globalThis.localStorage) {\n this.available = this.checkStorage();\n this.sessionId = this.loadOrCreateSessionId();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n if (!this.available) return;\n this.storage.setItem(keyProgress(this.sessionId), JSON.stringify({ currentPageId, answers }));\n }\n\n loadProgress(): SessionProgress | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyProgress(this.sessionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as SessionProgress;\n } catch {\n return null;\n }\n }\n\n saveInput(questionId: string, value: string | string[]): void {\n if (!this.available) return;\n this.storage.setItem(keyInput(this.sessionId, questionId), JSON.stringify(value));\n }\n\n loadInput(questionId: string): string | string[] | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyInput(this.sessionId, questionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as string | string[];\n } catch {\n return null;\n }\n }\n\n clearInput(questionId: string): void {\n if (!this.available) return;\n this.storage.removeItem(keyInput(this.sessionId, questionId));\n }\n\n complete(): void {\n this.clearAll();\n if (!this.available) return;\n this.storage.setItem(keyCompleted(this.sessionId), String(Date.now()));\n }\n\n completedAt(): number | null {\n if (!this.available) return null;\n const completedAt = Number(this.storage.getItem(keyCompleted(this.sessionId)));\n return Number.isFinite(completedAt) && completedAt > 0 ? completedAt : null;\n }\n\n clearAll(): void {\n if (!this.available) return;\n const progressKey = keyProgress(this.sessionId);\n const inputPrefix = `car:input:${this.sessionId}:`;\n const keysToRemove: string[] = [progressKey, keyCompleted(this.sessionId)];\n\n for (let i = 0; i < this.storage.length; i++) {\n const k = this.storage.key(i);\n if (k?.startsWith(inputPrefix)) keysToRemove.push(k);\n }\n\n for (const k of keysToRemove) {\n this.storage.removeItem(k);\n }\n }\n\n private loadOrCreateSessionId(): string {\n if (!this.available) return crypto.randomUUID();\n const existing = this.storage.getItem(KEY_SESSION);\n if (existing) return existing;\n const id = crypto.randomUUID();\n this.storage.setItem(KEY_SESSION, id);\n return id;\n }\n\n private checkStorage(): boolean {\n try {\n const probe = '__car_probe__';\n this.storage.setItem(probe, '1');\n this.storage.removeItem(probe);\n return true;\n } catch {\n return false;\n }\n }\n}\n","// packages/core/src/chat-controller.ts\n\nimport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nimport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nimport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nimport { RuleEngine } from './rule-engine.js';\nimport { parseSchema } from './schema-parser.js';\nimport { SessionManager, type SessionStore } from './session-manager.js';\nimport type {\n AnswerMap,\n ChatControllerConfig,\n ChatError,\n ChatEvent,\n ChatOptions,\n ChatPage,\n ChatQuestion,\n ChatSchema,\n ErrorLogResult,\n RouteEntry,\n SubmitOutcome,\n} from './types.js';\n\ntype EventName = 'page:submit' | 'form:complete';\ntype Listener = (event: ChatEvent) => void;\n\nconst DEFAULT_OPTIONS: ChatOptions = {\n userResumable: true,\n localStorage: true,\n autoComplete: true,\n allowEdit: false,\n autoHideQuestions: 0,\n autoHideAnswers: 0,\n hideAnimation: 'fade',\n typingSpeed: 0,\n thinkingDelay: 0,\n thinkingText: '',\n showContinueIcon: true,\n};\n\n/** Copies an object without its undefined values, so they cannot hide a default. */\nfunction stripUndefined<T extends object>(source: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(source).filter(([, value]) => value !== undefined),\n ) as Partial<T>;\n}\n\n/**\n * Central orchestrator for the chat-and-react form flow.\n * Drives page navigation, branching, session persistence, and event emission.\n */\nexport class ChatController {\n private schema: ChatSchema | null = null;\n private options: ChatOptions = { ...DEFAULT_OPTIONS };\n private readonly config: Required<ChatControllerConfig>;\n private readonly session: SessionStore;\n private readonly ruleEngine = new RuleEngine();\n private currentPage: ChatPage | null = null;\n private allAnswers: AnswerMap = {};\n private visitIndex = 0;\n /** Pages submitted in this session, in visit order. Used for editing earlier answers. */\n private route: RouteEntry[] = [];\n /** Index in `route` of the page being edited, or null when not editing. */\n private editIndex: number | null = null;\n /** The page the user was on before they started editing an earlier one. */\n private resumePageId: string | null = null;\n private readonly listeners = new Map<EventName, Set<Listener>>();\n\n /**\n * @param config - Controller configuration including schema and optional adapters.\n * @param sessionManager - Optional session manager for test injection.\n */\n constructor(config: ChatControllerConfig, sessionManager?: SessionStore) {\n this.config = {\n inputAdapter: config.inputAdapter ?? new IdentityInputAdapter(),\n outputAdapter: config.outputAdapter ?? new BrowserEventAdapter(),\n errorAdapter: config.errorAdapter ?? new ConsoleErrorAdapter(),\n schema: config.schema,\n options: config.options ?? {},\n };\n this.session = sessionManager ?? new SessionManager();\n }\n\n /**\n * Initialises the controller: parses the schema, resolves options,\n * restores any saved session, and positions to the first page.\n */\n start(): void {\n let raw: unknown;\n try {\n raw = this.config.inputAdapter.transform(this.config.schema);\n } catch (cause) {\n this.handleFatal({ code: 'ADAPTER_ERROR', message: 'InputAdapter.transform() threw', cause });\n }\n\n let schema: ChatSchema;\n try {\n schema = parseSchema(raw);\n } catch (e) {\n this.handleFatal(e as ChatError);\n }\n this.schema = schema;\n\n if (!this.session.isStorageAvailable) {\n this.logError({\n code: 'STORAGE_UNAVAILABLE',\n message: 'localStorage unavailable; running in memory-only mode',\n });\n }\n\n // Defaults, then the schema, then overrides passed in by the host (for\n // example element attributes).\n const schemaOpts = schema['x-chat-options'] ?? {};\n this.options = {\n ...DEFAULT_OPTIONS,\n ...stripUndefined(schemaOpts),\n ...stripUndefined(this.config.options),\n };\n\n if (this.options.userResumable) {\n const saved = this.session.loadProgress();\n if (saved) {\n const page = schema['x-chat-pages'].find((p) => p.id === saved.currentPageId);\n if (page) {\n this.allAnswers = saved.answers;\n this.currentPage = page;\n return;\n }\n }\n }\n\n this.currentPage = schema['x-chat-pages'][0];\n }\n\n /**\n * Returns the page currently being presented to the user, or null if complete.\n */\n getCurrentPage(): ChatPage | null {\n return this.currentPage;\n }\n\n /**\n * Returns the subset of questions on the current page that pass their\n * `x-chat-condition` rule (if any). Questions without a condition are always visible.\n */\n getVisibleQuestions(): ChatQuestion[] {\n if (!this.currentPage) return [];\n return this.currentPage.questions.filter((q) => this.isQuestionVisible(q));\n }\n\n /**\n * Returns the resolved options for the current session (schema options merged with defaults).\n */\n getOptions(): ChatOptions {\n return { ...this.options };\n }\n\n /**\n * Returns the pages the user has submitted in this session, in visit order.\n * A session restored from storage starts with an empty route.\n */\n getRoute(): RouteEntry[] {\n return this.route.map((entry) => ({ pageId: entry.pageId, answers: { ...entry.answers } }));\n }\n\n /**\n * Returns every answer given so far, for pre-filling inputs.\n */\n getAnswers(): AnswerMap {\n return { ...this.allAnswers };\n }\n\n /**\n * Re-opens a page the user already submitted so its answers can be changed.\n * Call `submitPage` afterwards to save the new answers. Not available once the\n * form is complete.\n *\n * @param routeIndex - Position of the page in `getRoute()`.\n * @returns true when the page was re-opened.\n */\n reopenPage(routeIndex: number): boolean {\n if (!this.schema || !this.currentPage) return false;\n const entry = this.route[routeIndex];\n if (!entry) return false;\n const page = this.schema['x-chat-pages'].find((p) => p.id === entry.pageId);\n if (!page) return false;\n\n // Remember where the user was, but only the first time: editing another\n // earlier page before finishing this edit must still return to that page.\n if (this.editIndex === null) this.resumePageId = this.currentPage.id;\n this.editIndex = routeIndex;\n this.currentPage = page;\n return true;\n }\n\n /**\n * Submits the answers for the current page and advances the session.\n * - Accumulates answers into `allAnswers`.\n * - Emits `page:submit` (suppressed when `userResumable` is false).\n * - Resolves branching to find the next page.\n * - Emits `form:complete` and clears session when `**FORM_COMPLETED**` is reached.\n *\n * When the page was re-opened with `reopenPage`, the new answers replace the\n * old ones and `page:submit` is emitted with `edited: true`. If the page's\n * branching still leads to the same next page, the form returns to the page the\n * user was on. If it leads somewhere else, the pages that followed it are\n * dropped, along with their answers, and the form continues from the new page.\n *\n * @param pageAnswers - Answers collected for the current page.\n */\n submitPage(pageAnswers: AnswerMap): SubmitOutcome {\n if (!this.currentPage || !this.schema) return 'advanced';\n\n const pageId = this.currentPage.id;\n const currentVisitIndex = this.visitIndex;\n const editedIndex = this.editIndex;\n const isEdit = editedIndex !== null;\n\n let outcome: SubmitOutcome = 'advanced';\n let nextPageId: string;\n let resumePageId: string | null = null;\n\n if (isEdit) {\n const entry = this.route[editedIndex];\n // Replace the old answers for this page, including any question that is\n // no longer visible after the change.\n const remaining = { ...this.allAnswers };\n for (const questionId of Object.keys(entry.answers)) delete remaining[questionId];\n this.allAnswers = { ...remaining, ...pageAnswers };\n entry.answers = { ...pageAnswers };\n\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n const expectedNextPageId = this.route[editedIndex + 1]?.pageId ?? this.resumePageId;\n\n if (nextPageId === expectedNextPageId) {\n outcome = 'edit-resumed';\n resumePageId = this.resumePageId;\n } else {\n outcome = 'edit-rerouted';\n // The route changed: forget the pages that followed and their answers.\n for (const later of this.route.slice(editedIndex + 1)) {\n for (const questionId of Object.keys(later.answers)) delete this.allAnswers[questionId];\n }\n this.route.length = editedIndex + 1;\n }\n this.editIndex = null;\n this.resumePageId = null;\n } else {\n this.allAnswers = { ...this.allAnswers, ...pageAnswers };\n this.route.push({ pageId, answers: { ...pageAnswers } });\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n }\n\n if (this.options.userResumable) {\n const progressPageId =\n outcome === 'edit-resumed' && resumePageId\n ? resumePageId\n : nextPageId === '**FORM_COMPLETED**'\n ? pageId\n : nextPageId;\n this.session.saveProgress(progressPageId, this.allAnswers);\n\n const submitEvent: ChatEvent = {\n type: 'page:submit',\n sessionId: this.session.getSessionId(),\n pageId,\n visitIndex: currentVisitIndex,\n answers: pageAnswers,\n ...(isEdit ? { edited: true } : {}),\n };\n this.safeEmit(submitEvent);\n this.emit('page:submit', submitEvent);\n }\n\n this.visitIndex++;\n\n if (outcome === 'edit-resumed' && resumePageId) {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === resumePageId) ?? null;\n return outcome;\n }\n\n if (nextPageId === '**FORM_COMPLETED**') {\n if (this.session.complete) {\n this.session.complete();\n } else {\n this.session.clearAll();\n }\n const completeEvent: ChatEvent = {\n type: 'form:complete',\n sessionId: this.session.getSessionId(),\n allAnswers: this.allAnswers,\n };\n this.safeEmit(completeEvent);\n this.emit('form:complete', completeEvent);\n this.currentPage = null;\n } else {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === nextPageId) ?? null;\n }\n return outcome;\n }\n\n /**\n * Registers an event listener for the given event type.\n *\n * @param event - The event to listen for.\n * @param listener - The callback to invoke when the event fires.\n */\n on(event: EventName, listener: Listener): void {\n let listenersForEvent = this.listeners.get(event);\n if (!listenersForEvent) {\n listenersForEvent = new Set();\n this.listeners.set(event, listenersForEvent);\n }\n listenersForEvent.add(listener);\n }\n\n /**\n * Removes a previously registered event listener.\n *\n * @param event - The event type to deregister from.\n * @param listener - The specific listener to remove.\n */\n off(event: EventName, listener: Listener): void {\n this.listeners.get(event)?.delete(listener);\n }\n\n /**\n * Tears down the controller: removes all listeners and nulls internal state.\n */\n destroy(): void {\n this.session.dispose?.();\n this.listeners.clear();\n this.currentPage = null;\n this.schema = null;\n }\n\n /**\n * Evaluates whether a question should be shown based on its `x-chat-condition`.\n * Returns true when no condition is present. On evaluation failure, logs and returns false.\n */\n private isQuestionVisible(question: ChatQuestion): boolean {\n const condition = question['x-chat-condition'];\n if (!condition) return true;\n\n const rules = this.schema?.['x-chat-rules'] ?? {};\n let rule: Record<string, unknown>;\n\n if ('ref' in condition && typeof (condition as { ref?: unknown }).ref === 'string') {\n const ref = (condition as { ref: string }).ref;\n rule = rules[ref];\n if (!rule) return false;\n } else {\n rule = condition as Record<string, unknown>;\n }\n\n try {\n return this.ruleEngine.evaluate(rule, this.answersForRules());\n } catch (e) {\n this.logError(e as ChatError);\n return false;\n }\n }\n\n /**\n * The answers question conditions are checked against. While an earlier page is\n * being edited, that page's own answers are left out, so its questions are\n * shown exactly as they were the first time the page was asked.\n */\n private answersForRules(): AnswerMap {\n if (this.editIndex === null) return this.allAnswers;\n const rest = { ...this.allAnswers };\n for (const questionId of Object.keys(this.route[this.editIndex].answers))\n delete rest[questionId];\n return rest;\n }\n\n /**\n * Walks the branching rules top-to-bottom. Returns the first match's destination_id.\n * Falls back to `**FORM_COMPLETED**` if nothing matches (should not happen with valid schemas).\n */\n private resolveBranching(page: ChatPage, answers: AnswerMap): string {\n for (const rule of page.branching) {\n if (rule.type === 'always') return rule.destination_id;\n if (rule.type === 'if' && answers[rule.form_element] === rule.value) {\n return rule.destination_id;\n }\n }\n return '**FORM_COMPLETED**';\n }\n\n /**\n * Calls `outputAdapter.emit()` and swallows adapter errors (logging them instead).\n */\n private safeEmit(event: ChatEvent): void {\n try {\n this.config.outputAdapter.emit(event);\n } catch (cause) {\n this.logError({\n code: 'ADAPTER_ERROR',\n message: `OutputAdapter.emit() threw for ${event.type}`,\n cause,\n });\n }\n }\n\n /**\n * Notifies all registered listeners for the given event.\n */\n private emit(event: EventName, payload: ChatEvent): void {\n this.listeners.get(event)?.forEach((l) => {\n l(payload);\n });\n }\n\n /**\n * Logs the error via the error adapter and then throws it.\n * Used for unrecoverable errors (schema invalid, adapter failure on start).\n */\n private handleFatal(error: ChatError): never {\n this.logError(error);\n throw error;\n }\n\n /**\n * Sends the error to the error adapter without waiting for it. If the adapter\n * reports a failure, rejects, or throws, that is written to `console.error` so\n * the original error is never lost and the form keeps running.\n */\n private logError(entry: ChatError): void {\n const reportFailure = (reason: unknown): void => {\n console.error('[chat-and-react] ErrorLogAdapter failed to record an error:', reason, entry);\n };\n try {\n // `Promise.resolve` also tolerates older custom adapters that return nothing.\n void Promise.resolve(this.config.errorAdapter.log(entry)).then(\n (result: ErrorLogResult | undefined): void => {\n if (result?.ok === false) {\n reportFailure(result.error);\n }\n },\n reportFailure,\n );\n } catch (cause) {\n reportFailure(cause);\n }\n }\n}\n","import type {\n StorageAdapter,\n StoredValue,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\nimport type { SessionProgress, SessionStore } from '../session-manager.js';\nimport type { AnswerMap, ChatError } from '../types.js';\n\nconst KEY_SESSION = 'car:session-id';\nconst keyRecord = (sessionId: string): string => `car:session:${sessionId}`;\n\n/** After this many revision conflicts in a row, a save is abandoned and reported. */\nconst MAX_CONFLICT_RETRIES = 3;\n\ntype InputValue = string | string[];\n\n/** Everything saved for one session, stored under a single key. */\ninterface SessionRecord {\n progress: SessionProgress | null;\n inputs: Record<string, InputValue>;\n /** When the form was finished (milliseconds since the epoch), if it has been. */\n completedAt?: number;\n}\n\nexport interface AdapterSessionManagerOptions {\n /** Called when the adapter fails. The session keeps working from memory. */\n onError?: (error: ChatError) => void;\n /**\n * Called when saved progress changes because of a write made elsewhere, such as\n * another tab. `null` means the session was cleared there.\n */\n onExternalChange?: (progress: SessionProgress | null) => void;\n}\n\nconst emptyRecord = (): SessionRecord => ({ progress: null, inputs: {} });\n\nfunction parseRecord(raw: string): SessionRecord | null {\n try {\n const parsed = JSON.parse(raw) as Partial<SessionRecord>;\n return {\n progress: parsed.progress ?? null,\n inputs: parsed.inputs && typeof parsed.inputs === 'object' ? parsed.inputs : {},\n ...(typeof parsed.completedAt === 'number' ? { completedAt: parsed.completedAt } : {}),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * A session store backed by a `StorageAdapter`. `ChatController` is synchronous, so\n * the saved session is loaded into memory by `create()`, reads come from memory, and\n * writes are saved in the background. Failures are reported through `onError`.\n *\n * Saves carry the revision they were based on. If another tab wrote first, the two\n * are merged: inputs per question, and progress only when this tab saved progress\n * itself, so a tab that only typed cannot overwrite another tab's progress.\n */\nexport class AdapterSessionManager implements SessionStore {\n private record: SessionRecord;\n private revision: string | undefined;\n private readonly dirtyInputs = new Set<string>();\n private dirtyProgress = false;\n /** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */\n private resetPending = false;\n private pending = false;\n private persisting: Promise<void> | null = null;\n private readonly signal: AbortSignal = new AbortController().signal;\n private unsubscribe: (() => void) | null = null;\n\n private constructor(\n private readonly adapter: StorageAdapter,\n private readonly sessionId: string,\n private readonly available: boolean,\n initial: StoredValue | null,\n private readonly options: AdapterSessionManagerOptions,\n ) {\n const parsed: SessionRecord | null = initial ? parseRecord(initial.value) : null;\n this.record = parsed ?? emptyRecord();\n this.revision = initial?.revision;\n if (initial && !parsed) {\n this.report(\n `Saved session ${sessionId} could not be read as JSON; starting with an empty session`,\n );\n }\n if (available) {\n this.unsubscribe =\n adapter.subscribe?.(keyRecord(sessionId), (stored) => this.applyRemote(stored)) ?? null;\n }\n }\n\n /** Loads (or creates) the session id and its saved record, then returns the manager. */\n static async create(\n adapter: StorageAdapter,\n options: AdapterSessionManagerOptions = {},\n ): Promise<AdapterSessionManager> {\n const signal: AbortSignal = new AbortController().signal;\n const fail = (message: string): void =>\n options.onError?.({ code: 'STORAGE_UNAVAILABLE', message });\n\n let sessionId: string | null = null;\n let available = true;\n\n const idResult: StoreResult<StoredValue | null> = await adapter.get(KEY_SESSION, { signal });\n if (idResult.ok) {\n sessionId = idResult.value?.value ?? null;\n } else {\n available = false;\n fail(`Could not read the session id: ${idResult.error}. Running in memory-only mode`);\n }\n if (available && sessionId === null) {\n sessionId = crypto.randomUUID();\n const saved: StoreResult<StoredValue> = await adapter.set(KEY_SESSION, sessionId, {\n signal,\n });\n if (!saved.ok) {\n available = false;\n fail(`Could not save the session id: ${saved.error}. Running in memory-only mode`);\n }\n }\n const id: string = sessionId ?? crypto.randomUUID();\n\n let initial: StoredValue | null = null;\n if (available) {\n const recordResult: StoreResult<StoredValue | null> = await adapter.get(keyRecord(id), {\n signal,\n });\n if (recordResult.ok) {\n initial = recordResult.value;\n } else {\n available = false;\n fail(`Could not read saved progress: ${recordResult.error}. Running in memory-only mode`);\n }\n }\n return new AdapterSessionManager(adapter, id, available, initial, options);\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n this.record.progress = { currentPageId, answers };\n this.dirtyProgress = true;\n this.schedulePersist();\n }\n\n loadProgress(): SessionProgress | null {\n return this.record.progress;\n }\n\n saveInput(questionId: string, value: InputValue): void {\n this.record.inputs[questionId] = value;\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n loadInput(questionId: string): InputValue | null {\n return this.record.inputs[questionId] ?? null;\n }\n\n clearInput(questionId: string): void {\n if (!(questionId in this.record.inputs)) return;\n delete this.record.inputs[questionId];\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n complete(): void {\n this.record = { progress: null, inputs: {}, completedAt: Date.now() };\n this.dirtyInputs.clear();\n this.dirtyProgress = true;\n this.resetPending = true;\n this.schedulePersist();\n }\n\n completedAt(): number | null {\n return this.record.completedAt ?? null;\n }\n\n clearAll(): void {\n this.record = emptyRecord();\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.resetPending = false;\n this.pending = false;\n if (!this.available) return;\n void this.removeRecord();\n }\n\n dispose(): void {\n this.unsubscribe?.();\n this.unsubscribe = null;\n }\n\n /** Resolves when every save made so far has finished. Mostly for tests. */\n async flush(): Promise<void> {\n while (this.persisting) {\n await this.persisting;\n }\n }\n\n private report(message: string, cause?: unknown): void {\n this.options.onError?.({ code: 'STORAGE_UNAVAILABLE', message, cause });\n }\n\n private schedulePersist(): void {\n if (!this.available) return;\n this.pending = true;\n this.persisting ??= this.drain();\n }\n\n /** Saves until nothing is left to save. Never rejects. */\n private async drain(): Promise<void> {\n let conflicts = 0;\n try {\n while (this.pending) {\n this.pending = false;\n const outcome: 'saved' | 'conflict' | 'failed' = await this.persistOnce();\n conflicts = outcome === 'conflict' ? conflicts + 1 : 0;\n if (outcome === 'conflict') {\n if (conflicts >= MAX_CONFLICT_RETRIES) {\n this.report(\n `Gave up saving session ${this.sessionId} after ${conflicts} revision conflicts in a row`,\n );\n break;\n }\n this.pending = true;\n }\n }\n } finally {\n // Cleared in the same tick the loop ends, so a save requested right now starts a new drain.\n this.persisting = null;\n }\n }\n\n private async persistOnce(): Promise<'saved' | 'conflict' | 'failed'> {\n const sentInputs = new Set<string>(this.dirtyInputs);\n const sentProgress: boolean = this.dirtyProgress;\n const sentReset: boolean = this.resetPending;\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.resetPending = false;\n\n const result: StoreResult<StoredValue> = await this.adapter.set(\n keyRecord(this.sessionId),\n JSON.stringify(this.record),\n { signal: this.signal, ifRevision: this.revision },\n );\n if (result.ok) {\n this.revision = result.value.revision;\n return 'saved';\n }\n\n // Keep what was not saved so the next attempt includes it.\n for (const questionId of sentInputs) this.dirtyInputs.add(questionId);\n this.dirtyProgress = this.dirtyProgress || sentProgress;\n this.resetPending = this.resetPending || sentReset;\n\n if (result.code !== 'conflict') {\n this.report(`Could not save session ${this.sessionId}: ${result.error}`);\n return 'failed';\n }\n const latest: StoreResult<StoredValue | null> = await this.adapter.get(\n keyRecord(this.sessionId),\n { signal: this.signal },\n );\n if (!latest.ok) {\n this.report(\n `Session ${this.sessionId} changed elsewhere, but the new version could not be read: ${latest.error}`,\n );\n return 'failed';\n }\n this.applyRemote(latest.value);\n return 'conflict';\n }\n\n /** Merges a version written elsewhere with this tab's unsaved changes. */\n private applyRemote(stored: StoredValue | null): void {\n const before: string = JSON.stringify(this.record.progress);\n if (!stored) {\n if (!this.dirtyProgress && this.dirtyInputs.size === 0) {\n this.record = emptyRecord();\n }\n this.revision = undefined;\n } else {\n const remote: SessionRecord | null = parseRecord(stored.value);\n if (!remote) {\n this.report(\n `Session ${this.sessionId} was changed elsewhere to a value that is not valid JSON; ignoring it`,\n );\n this.revision = stored.revision;\n return;\n }\n // A finished form leaves nothing of another tab's behind; otherwise the two are merged.\n const inputs: Record<string, InputValue> = this.resetPending ? {} : { ...remote.inputs };\n for (const questionId of this.dirtyInputs) {\n const mine: InputValue | undefined = this.record.inputs[questionId];\n // An input that is dirty but missing here was cleared on this tab.\n if (mine !== undefined) {\n inputs[questionId] = mine;\n } else {\n delete inputs[questionId];\n }\n }\n const completedAt: number | undefined =\n Math.max(this.record.completedAt ?? 0, remote.completedAt ?? 0) || undefined;\n this.record = {\n progress: this.dirtyProgress ? this.record.progress : remote.progress,\n inputs,\n ...(completedAt !== undefined ? { completedAt } : {}),\n };\n this.revision = stored.revision;\n }\n if (JSON.stringify(this.record.progress) !== before) {\n this.options.onExternalChange?.(this.record.progress);\n }\n }\n\n private async removeRecord(): Promise<void> {\n const key: string = keyRecord(this.sessionId);\n const result: StoreResult<unknown> = this.adapter.remove\n ? await this.adapter.remove(key, { signal: this.signal })\n : await this.adapter.set(key, JSON.stringify(emptyRecord()), { signal: this.signal });\n if (result.ok) {\n this.revision = undefined;\n } else {\n this.report(`Could not clear session ${this.sessionId}: ${result.error}`);\n }\n }\n}\n"],"mappings":"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","names":["BaseConsoleErrorAdapter","jsonLogic","KEY_SESSION"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { StorageAdapter } from '@richardmcquiston01/storage-adapters';
|
|
2
|
-
export { StorageAdapter
|
|
3
|
-
export { LocalStorageAdapterOptions, SessionStorageAdapterOptions, StorageChangeEvent, StorageEventTarget, WebStorageAdapterOptions, WebStorageLike, WebStorageName, localStorageAdapter, sessionStorageAdapter } from '@richardmcquiston01/storage-adapters/browser';
|
|
4
|
-
export { IndexedDbAdapterOptions, indexedDbAdapter } from '@richardmcquiston01/storage-adapters/indexeddb';
|
|
2
|
+
export { StorageAdapter } from '@richardmcquiston01/storage-adapters';
|
|
5
3
|
import { ErrorLogEntry, ErrorLogAdapter as ErrorLogAdapter$1, ConsoleErrorAdapter as ConsoleErrorAdapter$1, ConsoleErrorOptions } from '@richardmcquiston01/error-log-adapter';
|
|
6
4
|
export { ErrorLogEntry, ErrorLogResult } from '@richardmcquiston01/error-log-adapter';
|
|
7
5
|
|
|
@@ -173,6 +171,14 @@ interface SessionStore {
|
|
|
173
171
|
/** Forgets a saved input, such as a draft that has been submitted. */
|
|
174
172
|
clearInput(questionId: string): void;
|
|
175
173
|
clearAll(): void;
|
|
174
|
+
/**
|
|
175
|
+
* Called when the form is finished. Clears the saved session like `clearAll()` and also records
|
|
176
|
+
* that it was completed, so another tab that is still open can tell "finished" from "cleared".
|
|
177
|
+
* A store without it is cleared with `clearAll()`.
|
|
178
|
+
*/
|
|
179
|
+
complete?(): void;
|
|
180
|
+
/** When `complete()` was last called, in any tab (milliseconds since the epoch), or null. */
|
|
181
|
+
completedAt?(): number | null;
|
|
176
182
|
/** Stops listening for changes made elsewhere. Called when the controller is destroyed. */
|
|
177
183
|
dispose?(): void;
|
|
178
184
|
}
|
|
@@ -188,6 +194,8 @@ declare class SessionManager implements SessionStore {
|
|
|
188
194
|
saveInput(questionId: string, value: string | string[]): void;
|
|
189
195
|
loadInput(questionId: string): string | string[] | null;
|
|
190
196
|
clearInput(questionId: string): void;
|
|
197
|
+
complete(): void;
|
|
198
|
+
completedAt(): number | null;
|
|
191
199
|
clearAll(): void;
|
|
192
200
|
private loadOrCreateSessionId;
|
|
193
201
|
private checkStorage;
|
|
@@ -327,21 +335,6 @@ declare class ChatController {
|
|
|
327
335
|
private logError;
|
|
328
336
|
}
|
|
329
337
|
|
|
330
|
-
/**
|
|
331
|
-
* Evaluates JSON Logic rules against provided answers.
|
|
332
|
-
* Wraps json-logic-js to provide type-safe rule evaluation.
|
|
333
|
-
*/
|
|
334
|
-
declare class RuleEngine {
|
|
335
|
-
/**
|
|
336
|
-
* Evaluates a JSON Logic rule against an answer map.
|
|
337
|
-
* @param rule The JSON Logic rule to evaluate
|
|
338
|
-
* @param answers The answer map to evaluate the rule against
|
|
339
|
-
* @returns The boolean result of the rule evaluation
|
|
340
|
-
* @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails
|
|
341
|
-
*/
|
|
342
|
-
evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
338
|
/**
|
|
346
339
|
* Parses and validates an unknown input as a {@link ChatSchema}.
|
|
347
340
|
*
|
|
@@ -379,6 +372,8 @@ declare class AdapterSessionManager implements SessionStore {
|
|
|
379
372
|
private revision;
|
|
380
373
|
private readonly dirtyInputs;
|
|
381
374
|
private dirtyProgress;
|
|
375
|
+
/** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
|
|
376
|
+
private resetPending;
|
|
382
377
|
private pending;
|
|
383
378
|
private persisting;
|
|
384
379
|
private readonly signal;
|
|
@@ -393,6 +388,8 @@ declare class AdapterSessionManager implements SessionStore {
|
|
|
393
388
|
saveInput(questionId: string, value: InputValue): void;
|
|
394
389
|
loadInput(questionId: string): InputValue | null;
|
|
395
390
|
clearInput(questionId: string): void;
|
|
391
|
+
complete(): void;
|
|
392
|
+
completedAt(): number | null;
|
|
396
393
|
clearAll(): void;
|
|
397
394
|
dispose(): void;
|
|
398
395
|
/** Resolves when every save made so far has finished. Mostly for tests. */
|
|
@@ -407,4 +404,4 @@ declare class AdapterSessionManager implements SessionStore {
|
|
|
407
404
|
private removeRecord;
|
|
408
405
|
}
|
|
409
406
|
|
|
410
|
-
export { AdapterSessionManager, type AdapterSessionManagerOptions, type AnswerMap, type BranchingAlways, type BranchingIf, type BranchingRule, BrowserEventAdapter, type ChatConditionRef, ChatController, type ChatControllerConfig, type ChatError, type ChatErrorCode, type ChatEvent, type ChatInputType, type ChatOptions, type ChatPage, type ChatQuestion, type ChatSchema, ConsoleErrorAdapter, type ErrorLogAdapter, type FormCompleteEvent, type HideAnimation, IdentityInputAdapter, type InputAdapter, type JsonLogicRule, type OutputAdapter, type PageSubmitEvent,
|
|
407
|
+
export { AdapterSessionManager, type AdapterSessionManagerOptions, type AnswerMap, type BranchingAlways, type BranchingIf, type BranchingRule, BrowserEventAdapter, type ChatConditionRef, ChatController, type ChatControllerConfig, type ChatError, type ChatErrorCode, type ChatEvent, type ChatInputType, type ChatOptions, type ChatPage, type ChatQuestion, type ChatSchema, ConsoleErrorAdapter, type ErrorLogAdapter, type FormCompleteEvent, type HideAnimation, IdentityInputAdapter, type InputAdapter, type JsonLogicRule, type OutputAdapter, type PageSubmitEvent, SessionManager, type SessionProgress, type SessionStore, parseSchema };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { StorageAdapter } from '@richardmcquiston01/storage-adapters';
|
|
2
|
-
export { StorageAdapter
|
|
3
|
-
export { LocalStorageAdapterOptions, SessionStorageAdapterOptions, StorageChangeEvent, StorageEventTarget, WebStorageAdapterOptions, WebStorageLike, WebStorageName, localStorageAdapter, sessionStorageAdapter } from '@richardmcquiston01/storage-adapters/browser';
|
|
4
|
-
export { IndexedDbAdapterOptions, indexedDbAdapter } from '@richardmcquiston01/storage-adapters/indexeddb';
|
|
2
|
+
export { StorageAdapter } from '@richardmcquiston01/storage-adapters';
|
|
5
3
|
import { ErrorLogEntry, ErrorLogAdapter as ErrorLogAdapter$1, ConsoleErrorAdapter as ConsoleErrorAdapter$1, ConsoleErrorOptions } from '@richardmcquiston01/error-log-adapter';
|
|
6
4
|
export { ErrorLogEntry, ErrorLogResult } from '@richardmcquiston01/error-log-adapter';
|
|
7
5
|
|
|
@@ -173,6 +171,14 @@ interface SessionStore {
|
|
|
173
171
|
/** Forgets a saved input, such as a draft that has been submitted. */
|
|
174
172
|
clearInput(questionId: string): void;
|
|
175
173
|
clearAll(): void;
|
|
174
|
+
/**
|
|
175
|
+
* Called when the form is finished. Clears the saved session like `clearAll()` and also records
|
|
176
|
+
* that it was completed, so another tab that is still open can tell "finished" from "cleared".
|
|
177
|
+
* A store without it is cleared with `clearAll()`.
|
|
178
|
+
*/
|
|
179
|
+
complete?(): void;
|
|
180
|
+
/** When `complete()` was last called, in any tab (milliseconds since the epoch), or null. */
|
|
181
|
+
completedAt?(): number | null;
|
|
176
182
|
/** Stops listening for changes made elsewhere. Called when the controller is destroyed. */
|
|
177
183
|
dispose?(): void;
|
|
178
184
|
}
|
|
@@ -188,6 +194,8 @@ declare class SessionManager implements SessionStore {
|
|
|
188
194
|
saveInput(questionId: string, value: string | string[]): void;
|
|
189
195
|
loadInput(questionId: string): string | string[] | null;
|
|
190
196
|
clearInput(questionId: string): void;
|
|
197
|
+
complete(): void;
|
|
198
|
+
completedAt(): number | null;
|
|
191
199
|
clearAll(): void;
|
|
192
200
|
private loadOrCreateSessionId;
|
|
193
201
|
private checkStorage;
|
|
@@ -327,21 +335,6 @@ declare class ChatController {
|
|
|
327
335
|
private logError;
|
|
328
336
|
}
|
|
329
337
|
|
|
330
|
-
/**
|
|
331
|
-
* Evaluates JSON Logic rules against provided answers.
|
|
332
|
-
* Wraps json-logic-js to provide type-safe rule evaluation.
|
|
333
|
-
*/
|
|
334
|
-
declare class RuleEngine {
|
|
335
|
-
/**
|
|
336
|
-
* Evaluates a JSON Logic rule against an answer map.
|
|
337
|
-
* @param rule The JSON Logic rule to evaluate
|
|
338
|
-
* @param answers The answer map to evaluate the rule against
|
|
339
|
-
* @returns The boolean result of the rule evaluation
|
|
340
|
-
* @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails
|
|
341
|
-
*/
|
|
342
|
-
evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
338
|
/**
|
|
346
339
|
* Parses and validates an unknown input as a {@link ChatSchema}.
|
|
347
340
|
*
|
|
@@ -379,6 +372,8 @@ declare class AdapterSessionManager implements SessionStore {
|
|
|
379
372
|
private revision;
|
|
380
373
|
private readonly dirtyInputs;
|
|
381
374
|
private dirtyProgress;
|
|
375
|
+
/** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
|
|
376
|
+
private resetPending;
|
|
382
377
|
private pending;
|
|
383
378
|
private persisting;
|
|
384
379
|
private readonly signal;
|
|
@@ -393,6 +388,8 @@ declare class AdapterSessionManager implements SessionStore {
|
|
|
393
388
|
saveInput(questionId: string, value: InputValue): void;
|
|
394
389
|
loadInput(questionId: string): InputValue | null;
|
|
395
390
|
clearInput(questionId: string): void;
|
|
391
|
+
complete(): void;
|
|
392
|
+
completedAt(): number | null;
|
|
396
393
|
clearAll(): void;
|
|
397
394
|
dispose(): void;
|
|
398
395
|
/** Resolves when every save made so far has finished. Mostly for tests. */
|
|
@@ -407,4 +404,4 @@ declare class AdapterSessionManager implements SessionStore {
|
|
|
407
404
|
private removeRecord;
|
|
408
405
|
}
|
|
409
406
|
|
|
410
|
-
export { AdapterSessionManager, type AdapterSessionManagerOptions, type AnswerMap, type BranchingAlways, type BranchingIf, type BranchingRule, BrowserEventAdapter, type ChatConditionRef, ChatController, type ChatControllerConfig, type ChatError, type ChatErrorCode, type ChatEvent, type ChatInputType, type ChatOptions, type ChatPage, type ChatQuestion, type ChatSchema, ConsoleErrorAdapter, type ErrorLogAdapter, type FormCompleteEvent, type HideAnimation, IdentityInputAdapter, type InputAdapter, type JsonLogicRule, type OutputAdapter, type PageSubmitEvent,
|
|
407
|
+
export { AdapterSessionManager, type AdapterSessionManagerOptions, type AnswerMap, type BranchingAlways, type BranchingIf, type BranchingRule, BrowserEventAdapter, type ChatConditionRef, ChatController, type ChatControllerConfig, type ChatError, type ChatErrorCode, type ChatEvent, type ChatInputType, type ChatOptions, type ChatPage, type ChatQuestion, type ChatSchema, ConsoleErrorAdapter, type ErrorLogAdapter, type FormCompleteEvent, type HideAnimation, IdentityInputAdapter, type InputAdapter, type JsonLogicRule, type OutputAdapter, type PageSubmitEvent, SessionManager, type SessionProgress, type SessionStore, parseSchema };
|
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
|
-
import {
|
|
3
|
-
localStorageAdapter,
|
|
4
|
-
sessionStorageAdapter
|
|
5
|
-
} from "@richardmcquiston01/storage-adapters/browser";
|
|
6
|
-
import { indexedDbAdapter } from "@richardmcquiston01/storage-adapters/indexeddb";
|
|
7
|
-
|
|
8
1
|
// src/adapters/browser-event-adapter.ts
|
|
9
2
|
var BrowserEventAdapter = class {
|
|
10
3
|
emit(event) {
|
|
@@ -152,6 +145,7 @@ function makeError(message) {
|
|
|
152
145
|
var KEY_SESSION = "car:session-id";
|
|
153
146
|
var keyProgress = (id) => `car:progress:${id}`;
|
|
154
147
|
var keyInput = (sessionId, questionId) => `car:input:${sessionId}:${questionId}`;
|
|
148
|
+
var keyCompleted = (sessionId) => `car:completed:${sessionId}`;
|
|
155
149
|
var SessionManager = class {
|
|
156
150
|
constructor(storage = globalThis.localStorage) {
|
|
157
151
|
this.storage = storage;
|
|
@@ -196,11 +190,21 @@ var SessionManager = class {
|
|
|
196
190
|
if (!this.available) return;
|
|
197
191
|
this.storage.removeItem(keyInput(this.sessionId, questionId));
|
|
198
192
|
}
|
|
193
|
+
complete() {
|
|
194
|
+
this.clearAll();
|
|
195
|
+
if (!this.available) return;
|
|
196
|
+
this.storage.setItem(keyCompleted(this.sessionId), String(Date.now()));
|
|
197
|
+
}
|
|
198
|
+
completedAt() {
|
|
199
|
+
if (!this.available) return null;
|
|
200
|
+
const completedAt = Number(this.storage.getItem(keyCompleted(this.sessionId)));
|
|
201
|
+
return Number.isFinite(completedAt) && completedAt > 0 ? completedAt : null;
|
|
202
|
+
}
|
|
199
203
|
clearAll() {
|
|
200
204
|
if (!this.available) return;
|
|
201
205
|
const progressKey = keyProgress(this.sessionId);
|
|
202
206
|
const inputPrefix = `car:input:${this.sessionId}:`;
|
|
203
|
-
const keysToRemove = [progressKey];
|
|
207
|
+
const keysToRemove = [progressKey, keyCompleted(this.sessionId)];
|
|
204
208
|
for (let i = 0; i < this.storage.length; i++) {
|
|
205
209
|
const k = this.storage.key(i);
|
|
206
210
|
if (k?.startsWith(inputPrefix)) keysToRemove.push(k);
|
|
@@ -440,7 +444,11 @@ var ChatController = class {
|
|
|
440
444
|
return outcome;
|
|
441
445
|
}
|
|
442
446
|
if (nextPageId === "**FORM_COMPLETED**") {
|
|
443
|
-
this.session.
|
|
447
|
+
if (this.session.complete) {
|
|
448
|
+
this.session.complete();
|
|
449
|
+
} else {
|
|
450
|
+
this.session.clearAll();
|
|
451
|
+
}
|
|
444
452
|
const completeEvent = {
|
|
445
453
|
type: "form:complete",
|
|
446
454
|
sessionId: this.session.getSessionId(),
|
|
@@ -598,7 +606,8 @@ function parseRecord(raw) {
|
|
|
598
606
|
const parsed = JSON.parse(raw);
|
|
599
607
|
return {
|
|
600
608
|
progress: parsed.progress ?? null,
|
|
601
|
-
inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {}
|
|
609
|
+
inputs: parsed.inputs && typeof parsed.inputs === "object" ? parsed.inputs : {},
|
|
610
|
+
...typeof parsed.completedAt === "number" ? { completedAt: parsed.completedAt } : {}
|
|
602
611
|
};
|
|
603
612
|
} catch {
|
|
604
613
|
return null;
|
|
@@ -612,6 +621,8 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
612
621
|
this.options = options;
|
|
613
622
|
this.dirtyInputs = /* @__PURE__ */ new Set();
|
|
614
623
|
this.dirtyProgress = false;
|
|
624
|
+
/** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */
|
|
625
|
+
this.resetPending = false;
|
|
615
626
|
this.pending = false;
|
|
616
627
|
this.persisting = null;
|
|
617
628
|
this.signal = new AbortController().signal;
|
|
@@ -694,10 +705,21 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
694
705
|
this.dirtyInputs.add(questionId);
|
|
695
706
|
this.schedulePersist();
|
|
696
707
|
}
|
|
708
|
+
complete() {
|
|
709
|
+
this.record = { progress: null, inputs: {}, completedAt: Date.now() };
|
|
710
|
+
this.dirtyInputs.clear();
|
|
711
|
+
this.dirtyProgress = true;
|
|
712
|
+
this.resetPending = true;
|
|
713
|
+
this.schedulePersist();
|
|
714
|
+
}
|
|
715
|
+
completedAt() {
|
|
716
|
+
return this.record.completedAt ?? null;
|
|
717
|
+
}
|
|
697
718
|
clearAll() {
|
|
698
719
|
this.record = emptyRecord();
|
|
699
720
|
this.dirtyInputs.clear();
|
|
700
721
|
this.dirtyProgress = false;
|
|
722
|
+
this.resetPending = false;
|
|
701
723
|
this.pending = false;
|
|
702
724
|
if (!this.available) return;
|
|
703
725
|
void this.removeRecord();
|
|
@@ -745,8 +767,10 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
745
767
|
async persistOnce() {
|
|
746
768
|
const sentInputs = new Set(this.dirtyInputs);
|
|
747
769
|
const sentProgress = this.dirtyProgress;
|
|
770
|
+
const sentReset = this.resetPending;
|
|
748
771
|
this.dirtyInputs.clear();
|
|
749
772
|
this.dirtyProgress = false;
|
|
773
|
+
this.resetPending = false;
|
|
750
774
|
const result = await this.adapter.set(
|
|
751
775
|
keyRecord(this.sessionId),
|
|
752
776
|
JSON.stringify(this.record),
|
|
@@ -758,6 +782,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
758
782
|
}
|
|
759
783
|
for (const questionId of sentInputs) this.dirtyInputs.add(questionId);
|
|
760
784
|
this.dirtyProgress = this.dirtyProgress || sentProgress;
|
|
785
|
+
this.resetPending = this.resetPending || sentReset;
|
|
761
786
|
if (result.code !== "conflict") {
|
|
762
787
|
this.report(`Could not save session ${this.sessionId}: ${result.error}`);
|
|
763
788
|
return "failed";
|
|
@@ -792,7 +817,7 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
792
817
|
this.revision = stored.revision;
|
|
793
818
|
return;
|
|
794
819
|
}
|
|
795
|
-
const inputs = { ...remote.inputs };
|
|
820
|
+
const inputs = this.resetPending ? {} : { ...remote.inputs };
|
|
796
821
|
for (const questionId of this.dirtyInputs) {
|
|
797
822
|
const mine = this.record.inputs[questionId];
|
|
798
823
|
if (mine !== void 0) {
|
|
@@ -801,9 +826,11 @@ var AdapterSessionManager = class _AdapterSessionManager {
|
|
|
801
826
|
delete inputs[questionId];
|
|
802
827
|
}
|
|
803
828
|
}
|
|
829
|
+
const completedAt = Math.max(this.record.completedAt ?? 0, remote.completedAt ?? 0) || void 0;
|
|
804
830
|
this.record = {
|
|
805
831
|
progress: this.dirtyProgress ? this.record.progress : remote.progress,
|
|
806
|
-
inputs
|
|
832
|
+
inputs,
|
|
833
|
+
...completedAt !== void 0 ? { completedAt } : {}
|
|
807
834
|
};
|
|
808
835
|
this.revision = stored.revision;
|
|
809
836
|
}
|
|
@@ -827,11 +854,7 @@ export {
|
|
|
827
854
|
ChatController,
|
|
828
855
|
ConsoleErrorAdapter,
|
|
829
856
|
IdentityInputAdapter,
|
|
830
|
-
RuleEngine,
|
|
831
857
|
SessionManager,
|
|
832
|
-
|
|
833
|
-
localStorageAdapter,
|
|
834
|
-
parseSchema,
|
|
835
|
-
sessionStorageAdapter
|
|
858
|
+
parseSchema
|
|
836
859
|
};
|
|
837
860
|
//# sourceMappingURL=index.mjs.map
|
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/adapters/browser-event-adapter.ts","../src/adapters/console-error-adapter.ts","../src/adapters/identity-input-adapter.ts","../src/rule-engine.ts","../src/schema-parser.ts","../src/session-manager.ts","../src/chat-controller.ts","../src/storage/adapter-session-manager.ts"],"sourcesContent":["import type { ChatEvent, OutputAdapter } from '../types.js';\n\nexport class BrowserEventAdapter implements OutputAdapter {\n emit(event: ChatEvent): void {\n if (typeof document === 'undefined') return;\n const name = event.type === 'page:submit' ? 'chat-page-submit' : 'chat-form-complete';\n document.dispatchEvent(new CustomEvent(name, { detail: event, bubbles: true }));\n }\n}\n","import {\n ConsoleErrorAdapter as BaseConsoleErrorAdapter,\n type ConsoleErrorOptions,\n} from '@richardmcquiston01/error-log-adapter';\nimport type { ChatErrorCode } from '../types.js';\n\n/** Writes errors to `console.error`, prefixed with `[chat-and-react]` unless overridden. */\nexport class ConsoleErrorAdapter extends BaseConsoleErrorAdapter<ChatErrorCode> {\n constructor(options: ConsoleErrorOptions = {}) {\n super({ prefix: '[chat-and-react]', ...options });\n }\n}\n","import type { InputAdapter } from '../types.js';\n\nexport class IdentityInputAdapter implements InputAdapter {\n transform(raw: unknown): unknown {\n return raw;\n }\n}\n","import jsonLogic from 'json-logic-js';\nimport type { AnswerMap, JsonLogicRule } from './types.js';\n\n/**\n * Evaluates JSON Logic rules against provided answers.\n * Wraps json-logic-js to provide type-safe rule evaluation.\n */\nexport class RuleEngine {\n /**\n * Evaluates a JSON Logic rule against an answer map.\n * @param rule The JSON Logic rule to evaluate\n * @param answers The answer map to evaluate the rule against\n * @returns The boolean result of the rule evaluation\n * @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails\n */\n evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean {\n try {\n // json-logic-js apply() returns any; cast to boolean\n return Boolean(\n (jsonLogic as { apply: (rule: unknown, data: unknown) => unknown }).apply(rule, answers),\n );\n } catch (cause) {\n throw { code: 'RULE_EVAL_FAILED', message: 'Rule evaluation failed', cause };\n }\n }\n}\n","import type { ChatPage, ChatSchema } from './types.js';\n\nconst VALID_INPUTS = new Set(['text', 'textarea', 'dropdown', 'radio', 'checkbox']);\nconst FORM_COMPLETED = '**FORM_COMPLETED**';\n\n/**\n * Parses and validates an unknown input as a {@link ChatSchema}.\n *\n * @param raw - The raw input to validate.\n * @returns The typed {@link ChatSchema} if valid.\n * @throws `{ code: 'SCHEMA_INVALID', message: string }` on any validation failure.\n */\nexport function parseSchema(raw: unknown): ChatSchema {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Schema must be a non-null object');\n }\n\n const obj = raw as Record<string, unknown>;\n\n if (obj['x-chat-version'] !== '1') {\n throw makeError('x-chat-version must be \"1\"');\n }\n\n if (!Array.isArray(obj['x-chat-pages'])) {\n throw makeError('x-chat-pages must be an array');\n }\n\n const pages = obj['x-chat-pages'] as unknown[];\n const rules = (\n typeof obj['x-chat-rules'] === 'object' && obj['x-chat-rules'] !== null\n ? obj['x-chat-rules']\n : {}\n ) as Record<string, unknown>;\n\n const pageIds = new Set<string>();\n const questionIds = new Set<string>();\n\n for (const page of pages) {\n validatePage(page, pageIds, questionIds, rules);\n }\n\n // Second pass: validate branching destinations now that all page ids are known\n for (const page of pages) {\n const p = page as ChatPage;\n for (const rule of p.branching) {\n if (rule.destination_id !== FORM_COMPLETED && !pageIds.has(rule.destination_id)) {\n throw makeError(\n `branching destination_id \"${rule.destination_id}\" does not reference a known page id`,\n );\n }\n if (rule.type === 'if' && !questionIds.has(rule.form_element)) {\n throw makeError(\n `branching form_element \"${rule.form_element}\" does not reference a known question id`,\n );\n }\n }\n }\n\n return raw as ChatSchema;\n}\n\n/**\n * Validates a single page object, collecting its id and question ids.\n *\n * @param raw - The raw page value.\n * @param pageIds - Set of already-seen page ids (mutated in place).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validatePage(\n raw: unknown,\n pageIds: Set<string>,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each page must be a non-null object');\n }\n const page = raw as Record<string, unknown>;\n\n if (typeof page['id'] !== 'string' || !page['id']) {\n throw makeError('Page id must be a non-empty string');\n }\n if (pageIds.has(page['id'] as string)) {\n throw makeError(`Duplicate page id \"${page['id']}\"`);\n }\n pageIds.add(page['id'] as string);\n\n if (!Array.isArray(page['questions'])) {\n throw makeError(`Page \"${page['id']}\" must have a questions array`);\n }\n\n const branching = page['branching'];\n if (!Array.isArray(branching) || (branching as unknown[]).length === 0) {\n throw makeError(`Page \"${page['id']}\" must have a non-empty branching array`);\n }\n\n const lastRule = (branching as unknown[])[branching.length - 1] as Record<string, unknown>;\n if (lastRule['type'] !== 'always') {\n throw makeError(`Page \"${page['id']}\" branching must end with a rule of type \"always\"`);\n }\n\n for (const q of page['questions'] as unknown[]) {\n validateQuestion(q, page['id'] as string, questionIds, rules);\n }\n}\n\n/**\n * Validates a single question object, collecting its id.\n *\n * @param raw - The raw question value.\n * @param pageId - The id of the containing page (for error messages).\n * @param questionIds - Set of already-seen question ids (mutated in place).\n * @param rules - The x-chat-rules map for condition ref resolution.\n */\nfunction validateQuestion(\n raw: unknown,\n pageId: string,\n questionIds: Set<string>,\n rules: Record<string, unknown>,\n): void {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw makeError('Each question must be a non-null object');\n }\n const q = raw as Record<string, unknown>;\n\n if (typeof q['id'] !== 'string' || !q['id']) {\n throw makeError(`In page \"${pageId}\": question id must be a non-empty string`);\n }\n if (questionIds.has(q['id'] as string)) {\n throw makeError(`Duplicate question id \"${q['id']}\"`);\n }\n questionIds.add(q['id'] as string);\n\n if (!VALID_INPUTS.has(q['x-chat-input'] as string)) {\n throw makeError(\n `Question \"${q['id']}\" has invalid x-chat-input \"${q['x-chat-input']}\". ` +\n `Must be one of: text, textarea, dropdown, radio, checkbox`,\n );\n }\n\n const condition = q['x-chat-condition'];\n if (condition && typeof condition === 'object' && !Array.isArray(condition)) {\n const c = condition as Record<string, unknown>;\n if ('ref' in c && typeof c['ref'] === 'string') {\n if (!(c['ref'] in rules)) {\n throw makeError(`Question \"${q['id']}\" x-chat-condition refs unknown rule \"${c['ref']}\"`);\n }\n }\n }\n}\n\n/**\n * Creates a schema validation error object.\n *\n * @param message - Human-readable description of the validation failure.\n * @returns An error object with `code: 'SCHEMA_INVALID'`.\n */\nfunction makeError(message: string): { code: 'SCHEMA_INVALID'; message: string } {\n return { code: 'SCHEMA_INVALID', message };\n}\n","// packages/core/src/session-manager.ts\nimport type { AnswerMap } from './types.js';\n\nexport interface StorageBackend {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n key(index: number): string | null;\n readonly length: number;\n}\n\nexport interface SessionProgress {\n currentPageId: string;\n answers: AnswerMap;\n}\n\nconst KEY_SESSION = 'car:session-id';\nconst keyProgress = (id: string) => `car:progress:${id}`;\nconst keyInput = (sessionId: string, questionId: string) => `car:input:${sessionId}:${questionId}`;\nconst keyCompleted = (sessionId: string) => `car:completed:${sessionId}`;\n\n/**\n * What `ChatController` needs from session storage. `SessionManager` keeps it in a\n * synchronous Web Storage object; `AdapterSessionManager` keeps it in a `StorageAdapter`.\n */\nexport interface SessionStore {\n readonly isStorageAvailable: boolean;\n getSessionId(): string;\n saveProgress(currentPageId: string, answers: AnswerMap): void;\n loadProgress(): SessionProgress | null;\n saveInput(questionId: string, value: string | string[]): void;\n loadInput(questionId: string): string | string[] | null;\n /** Forgets a saved input, such as a draft that has been submitted. */\n clearInput(questionId: string): void;\n clearAll(): void;\n /**\n * Called when the form is finished. Clears the saved session like `clearAll()` and also records\n * that it was completed, so another tab that is still open can tell \"finished\" from \"cleared\".\n * A store without it is cleared with `clearAll()`.\n */\n complete?(): void;\n /** When `complete()` was last called, in any tab (milliseconds since the epoch), or null. */\n completedAt?(): number | null;\n /** Stops listening for changes made elsewhere. Called when the controller is destroyed. */\n dispose?(): void;\n}\n\nexport class SessionManager implements SessionStore {\n private readonly sessionId: string;\n private readonly available: boolean;\n\n constructor(private readonly storage: StorageBackend = globalThis.localStorage) {\n this.available = this.checkStorage();\n this.sessionId = this.loadOrCreateSessionId();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n if (!this.available) return;\n this.storage.setItem(keyProgress(this.sessionId), JSON.stringify({ currentPageId, answers }));\n }\n\n loadProgress(): SessionProgress | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyProgress(this.sessionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as SessionProgress;\n } catch {\n return null;\n }\n }\n\n saveInput(questionId: string, value: string | string[]): void {\n if (!this.available) return;\n this.storage.setItem(keyInput(this.sessionId, questionId), JSON.stringify(value));\n }\n\n loadInput(questionId: string): string | string[] | null {\n if (!this.available) return null;\n const raw = this.storage.getItem(keyInput(this.sessionId, questionId));\n if (!raw) return null;\n try {\n return JSON.parse(raw) as string | string[];\n } catch {\n return null;\n }\n }\n\n clearInput(questionId: string): void {\n if (!this.available) return;\n this.storage.removeItem(keyInput(this.sessionId, questionId));\n }\n\n complete(): void {\n this.clearAll();\n if (!this.available) return;\n this.storage.setItem(keyCompleted(this.sessionId), String(Date.now()));\n }\n\n completedAt(): number | null {\n if (!this.available) return null;\n const completedAt = Number(this.storage.getItem(keyCompleted(this.sessionId)));\n return Number.isFinite(completedAt) && completedAt > 0 ? completedAt : null;\n }\n\n clearAll(): void {\n if (!this.available) return;\n const progressKey = keyProgress(this.sessionId);\n const inputPrefix = `car:input:${this.sessionId}:`;\n const keysToRemove: string[] = [progressKey, keyCompleted(this.sessionId)];\n\n for (let i = 0; i < this.storage.length; i++) {\n const k = this.storage.key(i);\n if (k?.startsWith(inputPrefix)) keysToRemove.push(k);\n }\n\n for (const k of keysToRemove) {\n this.storage.removeItem(k);\n }\n }\n\n private loadOrCreateSessionId(): string {\n if (!this.available) return crypto.randomUUID();\n const existing = this.storage.getItem(KEY_SESSION);\n if (existing) return existing;\n const id = crypto.randomUUID();\n this.storage.setItem(KEY_SESSION, id);\n return id;\n }\n\n private checkStorage(): boolean {\n try {\n const probe = '__car_probe__';\n this.storage.setItem(probe, '1');\n this.storage.removeItem(probe);\n return true;\n } catch {\n return false;\n }\n }\n}\n","// packages/core/src/chat-controller.ts\n\nimport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\nimport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\nimport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\nimport { RuleEngine } from './rule-engine.js';\nimport { parseSchema } from './schema-parser.js';\nimport { SessionManager, type SessionStore } from './session-manager.js';\nimport type {\n AnswerMap,\n ChatControllerConfig,\n ChatError,\n ChatEvent,\n ChatOptions,\n ChatPage,\n ChatQuestion,\n ChatSchema,\n ErrorLogResult,\n RouteEntry,\n SubmitOutcome,\n} from './types.js';\n\ntype EventName = 'page:submit' | 'form:complete';\ntype Listener = (event: ChatEvent) => void;\n\nconst DEFAULT_OPTIONS: ChatOptions = {\n userResumable: true,\n localStorage: true,\n autoComplete: true,\n allowEdit: false,\n autoHideQuestions: 0,\n autoHideAnswers: 0,\n hideAnimation: 'fade',\n typingSpeed: 0,\n thinkingDelay: 0,\n thinkingText: '',\n showContinueIcon: true,\n};\n\n/** Copies an object without its undefined values, so they cannot hide a default. */\nfunction stripUndefined<T extends object>(source: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(source).filter(([, value]) => value !== undefined),\n ) as Partial<T>;\n}\n\n/**\n * Central orchestrator for the chat-and-react form flow.\n * Drives page navigation, branching, session persistence, and event emission.\n */\nexport class ChatController {\n private schema: ChatSchema | null = null;\n private options: ChatOptions = { ...DEFAULT_OPTIONS };\n private readonly config: Required<ChatControllerConfig>;\n private readonly session: SessionStore;\n private readonly ruleEngine = new RuleEngine();\n private currentPage: ChatPage | null = null;\n private allAnswers: AnswerMap = {};\n private visitIndex = 0;\n /** Pages submitted in this session, in visit order. Used for editing earlier answers. */\n private route: RouteEntry[] = [];\n /** Index in `route` of the page being edited, or null when not editing. */\n private editIndex: number | null = null;\n /** The page the user was on before they started editing an earlier one. */\n private resumePageId: string | null = null;\n private readonly listeners = new Map<EventName, Set<Listener>>();\n\n /**\n * @param config - Controller configuration including schema and optional adapters.\n * @param sessionManager - Optional session manager for test injection.\n */\n constructor(config: ChatControllerConfig, sessionManager?: SessionStore) {\n this.config = {\n inputAdapter: config.inputAdapter ?? new IdentityInputAdapter(),\n outputAdapter: config.outputAdapter ?? new BrowserEventAdapter(),\n errorAdapter: config.errorAdapter ?? new ConsoleErrorAdapter(),\n schema: config.schema,\n options: config.options ?? {},\n };\n this.session = sessionManager ?? new SessionManager();\n }\n\n /**\n * Initialises the controller: parses the schema, resolves options,\n * restores any saved session, and positions to the first page.\n */\n start(): void {\n let raw: unknown;\n try {\n raw = this.config.inputAdapter.transform(this.config.schema);\n } catch (cause) {\n this.handleFatal({ code: 'ADAPTER_ERROR', message: 'InputAdapter.transform() threw', cause });\n }\n\n let schema: ChatSchema;\n try {\n schema = parseSchema(raw);\n } catch (e) {\n this.handleFatal(e as ChatError);\n }\n this.schema = schema;\n\n if (!this.session.isStorageAvailable) {\n this.logError({\n code: 'STORAGE_UNAVAILABLE',\n message: 'localStorage unavailable; running in memory-only mode',\n });\n }\n\n // Defaults, then the schema, then overrides passed in by the host (for\n // example element attributes).\n const schemaOpts = schema['x-chat-options'] ?? {};\n this.options = {\n ...DEFAULT_OPTIONS,\n ...stripUndefined(schemaOpts),\n ...stripUndefined(this.config.options),\n };\n\n if (this.options.userResumable) {\n const saved = this.session.loadProgress();\n if (saved) {\n const page = schema['x-chat-pages'].find((p) => p.id === saved.currentPageId);\n if (page) {\n this.allAnswers = saved.answers;\n this.currentPage = page;\n return;\n }\n }\n }\n\n this.currentPage = schema['x-chat-pages'][0];\n }\n\n /**\n * Returns the page currently being presented to the user, or null if complete.\n */\n getCurrentPage(): ChatPage | null {\n return this.currentPage;\n }\n\n /**\n * Returns the subset of questions on the current page that pass their\n * `x-chat-condition` rule (if any). Questions without a condition are always visible.\n */\n getVisibleQuestions(): ChatQuestion[] {\n if (!this.currentPage) return [];\n return this.currentPage.questions.filter((q) => this.isQuestionVisible(q));\n }\n\n /**\n * Returns the resolved options for the current session (schema options merged with defaults).\n */\n getOptions(): ChatOptions {\n return { ...this.options };\n }\n\n /**\n * Returns the pages the user has submitted in this session, in visit order.\n * A session restored from storage starts with an empty route.\n */\n getRoute(): RouteEntry[] {\n return this.route.map((entry) => ({ pageId: entry.pageId, answers: { ...entry.answers } }));\n }\n\n /**\n * Returns every answer given so far, for pre-filling inputs.\n */\n getAnswers(): AnswerMap {\n return { ...this.allAnswers };\n }\n\n /**\n * Re-opens a page the user already submitted so its answers can be changed.\n * Call `submitPage` afterwards to save the new answers. Not available once the\n * form is complete.\n *\n * @param routeIndex - Position of the page in `getRoute()`.\n * @returns true when the page was re-opened.\n */\n reopenPage(routeIndex: number): boolean {\n if (!this.schema || !this.currentPage) return false;\n const entry = this.route[routeIndex];\n if (!entry) return false;\n const page = this.schema['x-chat-pages'].find((p) => p.id === entry.pageId);\n if (!page) return false;\n\n // Remember where the user was, but only the first time: editing another\n // earlier page before finishing this edit must still return to that page.\n if (this.editIndex === null) this.resumePageId = this.currentPage.id;\n this.editIndex = routeIndex;\n this.currentPage = page;\n return true;\n }\n\n /**\n * Submits the answers for the current page and advances the session.\n * - Accumulates answers into `allAnswers`.\n * - Emits `page:submit` (suppressed when `userResumable` is false).\n * - Resolves branching to find the next page.\n * - Emits `form:complete` and clears session when `**FORM_COMPLETED**` is reached.\n *\n * When the page was re-opened with `reopenPage`, the new answers replace the\n * old ones and `page:submit` is emitted with `edited: true`. If the page's\n * branching still leads to the same next page, the form returns to the page the\n * user was on. If it leads somewhere else, the pages that followed it are\n * dropped, along with their answers, and the form continues from the new page.\n *\n * @param pageAnswers - Answers collected for the current page.\n */\n submitPage(pageAnswers: AnswerMap): SubmitOutcome {\n if (!this.currentPage || !this.schema) return 'advanced';\n\n const pageId = this.currentPage.id;\n const currentVisitIndex = this.visitIndex;\n const editedIndex = this.editIndex;\n const isEdit = editedIndex !== null;\n\n let outcome: SubmitOutcome = 'advanced';\n let nextPageId: string;\n let resumePageId: string | null = null;\n\n if (isEdit) {\n const entry = this.route[editedIndex];\n // Replace the old answers for this page, including any question that is\n // no longer visible after the change.\n const remaining = { ...this.allAnswers };\n for (const questionId of Object.keys(entry.answers)) delete remaining[questionId];\n this.allAnswers = { ...remaining, ...pageAnswers };\n entry.answers = { ...pageAnswers };\n\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n const expectedNextPageId = this.route[editedIndex + 1]?.pageId ?? this.resumePageId;\n\n if (nextPageId === expectedNextPageId) {\n outcome = 'edit-resumed';\n resumePageId = this.resumePageId;\n } else {\n outcome = 'edit-rerouted';\n // The route changed: forget the pages that followed and their answers.\n for (const later of this.route.slice(editedIndex + 1)) {\n for (const questionId of Object.keys(later.answers)) delete this.allAnswers[questionId];\n }\n this.route.length = editedIndex + 1;\n }\n this.editIndex = null;\n this.resumePageId = null;\n } else {\n this.allAnswers = { ...this.allAnswers, ...pageAnswers };\n this.route.push({ pageId, answers: { ...pageAnswers } });\n nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\n }\n\n if (this.options.userResumable) {\n const progressPageId =\n outcome === 'edit-resumed' && resumePageId\n ? resumePageId\n : nextPageId === '**FORM_COMPLETED**'\n ? pageId\n : nextPageId;\n this.session.saveProgress(progressPageId, this.allAnswers);\n\n const submitEvent: ChatEvent = {\n type: 'page:submit',\n sessionId: this.session.getSessionId(),\n pageId,\n visitIndex: currentVisitIndex,\n answers: pageAnswers,\n ...(isEdit ? { edited: true } : {}),\n };\n this.safeEmit(submitEvent);\n this.emit('page:submit', submitEvent);\n }\n\n this.visitIndex++;\n\n if (outcome === 'edit-resumed' && resumePageId) {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === resumePageId) ?? null;\n return outcome;\n }\n\n if (nextPageId === '**FORM_COMPLETED**') {\n if (this.session.complete) {\n this.session.complete();\n } else {\n this.session.clearAll();\n }\n const completeEvent: ChatEvent = {\n type: 'form:complete',\n sessionId: this.session.getSessionId(),\n allAnswers: this.allAnswers,\n };\n this.safeEmit(completeEvent);\n this.emit('form:complete', completeEvent);\n this.currentPage = null;\n } else {\n this.currentPage = this.schema['x-chat-pages'].find((p) => p.id === nextPageId) ?? null;\n }\n return outcome;\n }\n\n /**\n * Registers an event listener for the given event type.\n *\n * @param event - The event to listen for.\n * @param listener - The callback to invoke when the event fires.\n */\n on(event: EventName, listener: Listener): void {\n let listenersForEvent = this.listeners.get(event);\n if (!listenersForEvent) {\n listenersForEvent = new Set();\n this.listeners.set(event, listenersForEvent);\n }\n listenersForEvent.add(listener);\n }\n\n /**\n * Removes a previously registered event listener.\n *\n * @param event - The event type to deregister from.\n * @param listener - The specific listener to remove.\n */\n off(event: EventName, listener: Listener): void {\n this.listeners.get(event)?.delete(listener);\n }\n\n /**\n * Tears down the controller: removes all listeners and nulls internal state.\n */\n destroy(): void {\n this.session.dispose?.();\n this.listeners.clear();\n this.currentPage = null;\n this.schema = null;\n }\n\n /**\n * Evaluates whether a question should be shown based on its `x-chat-condition`.\n * Returns true when no condition is present. On evaluation failure, logs and returns false.\n */\n private isQuestionVisible(question: ChatQuestion): boolean {\n const condition = question['x-chat-condition'];\n if (!condition) return true;\n\n const rules = this.schema?.['x-chat-rules'] ?? {};\n let rule: Record<string, unknown>;\n\n if ('ref' in condition && typeof (condition as { ref?: unknown }).ref === 'string') {\n const ref = (condition as { ref: string }).ref;\n rule = rules[ref];\n if (!rule) return false;\n } else {\n rule = condition as Record<string, unknown>;\n }\n\n try {\n return this.ruleEngine.evaluate(rule, this.answersForRules());\n } catch (e) {\n this.logError(e as ChatError);\n return false;\n }\n }\n\n /**\n * The answers question conditions are checked against. While an earlier page is\n * being edited, that page's own answers are left out, so its questions are\n * shown exactly as they were the first time the page was asked.\n */\n private answersForRules(): AnswerMap {\n if (this.editIndex === null) return this.allAnswers;\n const rest = { ...this.allAnswers };\n for (const questionId of Object.keys(this.route[this.editIndex].answers))\n delete rest[questionId];\n return rest;\n }\n\n /**\n * Walks the branching rules top-to-bottom. Returns the first match's destination_id.\n * Falls back to `**FORM_COMPLETED**` if nothing matches (should not happen with valid schemas).\n */\n private resolveBranching(page: ChatPage, answers: AnswerMap): string {\n for (const rule of page.branching) {\n if (rule.type === 'always') return rule.destination_id;\n if (rule.type === 'if' && answers[rule.form_element] === rule.value) {\n return rule.destination_id;\n }\n }\n return '**FORM_COMPLETED**';\n }\n\n /**\n * Calls `outputAdapter.emit()` and swallows adapter errors (logging them instead).\n */\n private safeEmit(event: ChatEvent): void {\n try {\n this.config.outputAdapter.emit(event);\n } catch (cause) {\n this.logError({\n code: 'ADAPTER_ERROR',\n message: `OutputAdapter.emit() threw for ${event.type}`,\n cause,\n });\n }\n }\n\n /**\n * Notifies all registered listeners for the given event.\n */\n private emit(event: EventName, payload: ChatEvent): void {\n this.listeners.get(event)?.forEach((l) => {\n l(payload);\n });\n }\n\n /**\n * Logs the error via the error adapter and then throws it.\n * Used for unrecoverable errors (schema invalid, adapter failure on start).\n */\n private handleFatal(error: ChatError): never {\n this.logError(error);\n throw error;\n }\n\n /**\n * Sends the error to the error adapter without waiting for it. If the adapter\n * reports a failure, rejects, or throws, that is written to `console.error` so\n * the original error is never lost and the form keeps running.\n */\n private logError(entry: ChatError): void {\n const reportFailure = (reason: unknown): void => {\n console.error('[chat-and-react] ErrorLogAdapter failed to record an error:', reason, entry);\n };\n try {\n // `Promise.resolve` also tolerates older custom adapters that return nothing.\n void Promise.resolve(this.config.errorAdapter.log(entry)).then(\n (result: ErrorLogResult | undefined): void => {\n if (result?.ok === false) {\n reportFailure(result.error);\n }\n },\n reportFailure,\n );\n } catch (cause) {\n reportFailure(cause);\n }\n }\n}\n","import type {\n StorageAdapter,\n StoredValue,\n StoreResult,\n} from '@richardmcquiston01/storage-adapters';\nimport type { SessionProgress, SessionStore } from '../session-manager.js';\nimport type { AnswerMap, ChatError } from '../types.js';\n\nconst KEY_SESSION = 'car:session-id';\nconst keyRecord = (sessionId: string): string => `car:session:${sessionId}`;\n\n/** After this many revision conflicts in a row, a save is abandoned and reported. */\nconst MAX_CONFLICT_RETRIES = 3;\n\ntype InputValue = string | string[];\n\n/** Everything saved for one session, stored under a single key. */\ninterface SessionRecord {\n progress: SessionProgress | null;\n inputs: Record<string, InputValue>;\n /** When the form was finished (milliseconds since the epoch), if it has been. */\n completedAt?: number;\n}\n\nexport interface AdapterSessionManagerOptions {\n /** Called when the adapter fails. The session keeps working from memory. */\n onError?: (error: ChatError) => void;\n /**\n * Called when saved progress changes because of a write made elsewhere, such as\n * another tab. `null` means the session was cleared there.\n */\n onExternalChange?: (progress: SessionProgress | null) => void;\n}\n\nconst emptyRecord = (): SessionRecord => ({ progress: null, inputs: {} });\n\nfunction parseRecord(raw: string): SessionRecord | null {\n try {\n const parsed = JSON.parse(raw) as Partial<SessionRecord>;\n return {\n progress: parsed.progress ?? null,\n inputs: parsed.inputs && typeof parsed.inputs === 'object' ? parsed.inputs : {},\n ...(typeof parsed.completedAt === 'number' ? { completedAt: parsed.completedAt } : {}),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * A session store backed by a `StorageAdapter`. `ChatController` is synchronous, so\n * the saved session is loaded into memory by `create()`, reads come from memory, and\n * writes are saved in the background. Failures are reported through `onError`.\n *\n * Saves carry the revision they were based on. If another tab wrote first, the two\n * are merged: inputs per question, and progress only when this tab saved progress\n * itself, so a tab that only typed cannot overwrite another tab's progress.\n */\nexport class AdapterSessionManager implements SessionStore {\n private record: SessionRecord;\n private revision: string | undefined;\n private readonly dirtyInputs = new Set<string>();\n private dirtyProgress = false;\n /** True from `complete()` until it is saved: the whole session was reset, so nothing remote is kept. */\n private resetPending = false;\n private pending = false;\n private persisting: Promise<void> | null = null;\n private readonly signal: AbortSignal = new AbortController().signal;\n private unsubscribe: (() => void) | null = null;\n\n private constructor(\n private readonly adapter: StorageAdapter,\n private readonly sessionId: string,\n private readonly available: boolean,\n initial: StoredValue | null,\n private readonly options: AdapterSessionManagerOptions,\n ) {\n const parsed: SessionRecord | null = initial ? parseRecord(initial.value) : null;\n this.record = parsed ?? emptyRecord();\n this.revision = initial?.revision;\n if (initial && !parsed) {\n this.report(\n `Saved session ${sessionId} could not be read as JSON; starting with an empty session`,\n );\n }\n if (available) {\n this.unsubscribe =\n adapter.subscribe?.(keyRecord(sessionId), (stored) => this.applyRemote(stored)) ?? null;\n }\n }\n\n /** Loads (or creates) the session id and its saved record, then returns the manager. */\n static async create(\n adapter: StorageAdapter,\n options: AdapterSessionManagerOptions = {},\n ): Promise<AdapterSessionManager> {\n const signal: AbortSignal = new AbortController().signal;\n const fail = (message: string): void =>\n options.onError?.({ code: 'STORAGE_UNAVAILABLE', message });\n\n let sessionId: string | null = null;\n let available = true;\n\n const idResult: StoreResult<StoredValue | null> = await adapter.get(KEY_SESSION, { signal });\n if (idResult.ok) {\n sessionId = idResult.value?.value ?? null;\n } else {\n available = false;\n fail(`Could not read the session id: ${idResult.error}. Running in memory-only mode`);\n }\n if (available && sessionId === null) {\n sessionId = crypto.randomUUID();\n const saved: StoreResult<StoredValue> = await adapter.set(KEY_SESSION, sessionId, {\n signal,\n });\n if (!saved.ok) {\n available = false;\n fail(`Could not save the session id: ${saved.error}. Running in memory-only mode`);\n }\n }\n const id: string = sessionId ?? crypto.randomUUID();\n\n let initial: StoredValue | null = null;\n if (available) {\n const recordResult: StoreResult<StoredValue | null> = await adapter.get(keyRecord(id), {\n signal,\n });\n if (recordResult.ok) {\n initial = recordResult.value;\n } else {\n available = false;\n fail(`Could not read saved progress: ${recordResult.error}. Running in memory-only mode`);\n }\n }\n return new AdapterSessionManager(adapter, id, available, initial, options);\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n get isStorageAvailable(): boolean {\n return this.available;\n }\n\n saveProgress(currentPageId: string, answers: AnswerMap): void {\n this.record.progress = { currentPageId, answers };\n this.dirtyProgress = true;\n this.schedulePersist();\n }\n\n loadProgress(): SessionProgress | null {\n return this.record.progress;\n }\n\n saveInput(questionId: string, value: InputValue): void {\n this.record.inputs[questionId] = value;\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n loadInput(questionId: string): InputValue | null {\n return this.record.inputs[questionId] ?? null;\n }\n\n clearInput(questionId: string): void {\n if (!(questionId in this.record.inputs)) return;\n delete this.record.inputs[questionId];\n this.dirtyInputs.add(questionId);\n this.schedulePersist();\n }\n\n complete(): void {\n this.record = { progress: null, inputs: {}, completedAt: Date.now() };\n this.dirtyInputs.clear();\n this.dirtyProgress = true;\n this.resetPending = true;\n this.schedulePersist();\n }\n\n completedAt(): number | null {\n return this.record.completedAt ?? null;\n }\n\n clearAll(): void {\n this.record = emptyRecord();\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.resetPending = false;\n this.pending = false;\n if (!this.available) return;\n void this.removeRecord();\n }\n\n dispose(): void {\n this.unsubscribe?.();\n this.unsubscribe = null;\n }\n\n /** Resolves when every save made so far has finished. Mostly for tests. */\n async flush(): Promise<void> {\n while (this.persisting) {\n await this.persisting;\n }\n }\n\n private report(message: string, cause?: unknown): void {\n this.options.onError?.({ code: 'STORAGE_UNAVAILABLE', message, cause });\n }\n\n private schedulePersist(): void {\n if (!this.available) return;\n this.pending = true;\n this.persisting ??= this.drain();\n }\n\n /** Saves until nothing is left to save. Never rejects. */\n private async drain(): Promise<void> {\n let conflicts = 0;\n try {\n while (this.pending) {\n this.pending = false;\n const outcome: 'saved' | 'conflict' | 'failed' = await this.persistOnce();\n conflicts = outcome === 'conflict' ? conflicts + 1 : 0;\n if (outcome === 'conflict') {\n if (conflicts >= MAX_CONFLICT_RETRIES) {\n this.report(\n `Gave up saving session ${this.sessionId} after ${conflicts} revision conflicts in a row`,\n );\n break;\n }\n this.pending = true;\n }\n }\n } finally {\n // Cleared in the same tick the loop ends, so a save requested right now starts a new drain.\n this.persisting = null;\n }\n }\n\n private async persistOnce(): Promise<'saved' | 'conflict' | 'failed'> {\n const sentInputs = new Set<string>(this.dirtyInputs);\n const sentProgress: boolean = this.dirtyProgress;\n const sentReset: boolean = this.resetPending;\n this.dirtyInputs.clear();\n this.dirtyProgress = false;\n this.resetPending = false;\n\n const result: StoreResult<StoredValue> = await this.adapter.set(\n keyRecord(this.sessionId),\n JSON.stringify(this.record),\n { signal: this.signal, ifRevision: this.revision },\n );\n if (result.ok) {\n this.revision = result.value.revision;\n return 'saved';\n }\n\n // Keep what was not saved so the next attempt includes it.\n for (const questionId of sentInputs) this.dirtyInputs.add(questionId);\n this.dirtyProgress = this.dirtyProgress || sentProgress;\n this.resetPending = this.resetPending || sentReset;\n\n if (result.code !== 'conflict') {\n this.report(`Could not save session ${this.sessionId}: ${result.error}`);\n return 'failed';\n }\n const latest: StoreResult<StoredValue | null> = await this.adapter.get(\n keyRecord(this.sessionId),\n { signal: this.signal },\n );\n if (!latest.ok) {\n this.report(\n `Session ${this.sessionId} changed elsewhere, but the new version could not be read: ${latest.error}`,\n );\n return 'failed';\n }\n this.applyRemote(latest.value);\n return 'conflict';\n }\n\n /** Merges a version written elsewhere with this tab's unsaved changes. */\n private applyRemote(stored: StoredValue | null): void {\n const before: string = JSON.stringify(this.record.progress);\n if (!stored) {\n if (!this.dirtyProgress && this.dirtyInputs.size === 0) {\n this.record = emptyRecord();\n }\n this.revision = undefined;\n } else {\n const remote: SessionRecord | null = parseRecord(stored.value);\n if (!remote) {\n this.report(\n `Session ${this.sessionId} was changed elsewhere to a value that is not valid JSON; ignoring it`,\n );\n this.revision = stored.revision;\n return;\n }\n // A finished form leaves nothing of another tab's behind; otherwise the two are merged.\n const inputs: Record<string, InputValue> = this.resetPending ? {} : { ...remote.inputs };\n for (const questionId of this.dirtyInputs) {\n const mine: InputValue | undefined = this.record.inputs[questionId];\n // An input that is dirty but missing here was cleared on this tab.\n if (mine !== undefined) {\n inputs[questionId] = mine;\n } else {\n delete inputs[questionId];\n }\n }\n const completedAt: number | undefined =\n Math.max(this.record.completedAt ?? 0, remote.completedAt ?? 0) || undefined;\n this.record = {\n progress: this.dirtyProgress ? this.record.progress : remote.progress,\n inputs,\n ...(completedAt !== undefined ? { completedAt } : {}),\n };\n this.revision = stored.revision;\n }\n if (JSON.stringify(this.record.progress) !== before) {\n this.options.onExternalChange?.(this.record.progress);\n }\n }\n\n private async removeRecord(): Promise<void> {\n const key: string = keyRecord(this.sessionId);\n const result: StoreResult<unknown> = this.adapter.remove\n ? await this.adapter.remove(key, { signal: this.signal })\n : await this.adapter.set(key, JSON.stringify(emptyRecord()), { signal: this.signal });\n if (result.ok) {\n this.revision = undefined;\n } else {\n this.report(`Could not clear session ${this.sessionId}: ${result.error}`);\n }\n }\n}\n"],"mappings":"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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.12.0",
|
|
4
4
|
"description": "Headless controller, JSON Schema validation, and session management for chat-and-react — a framework-agnostic conversational form library.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Richard McQuiston <richard.mcquiston01@gmail.com>",
|