UNPKG

@loqate/compose

Version:

Loqate Compose SDK — high-level flows that compose base API clients into easy-to-use sessions.

1 lines 88.3 kB
{"version":3,"file":"index.cjs","names":["value: T","baseline: T","store: DeepStore<T>","out: any","cur: any","DEFAULT_GREEN_TEST: MatchRule","DEFAULT_ORANGE_TEST: MatchRule","noopLogger: BaseLogger","wrap: Handler<E[K]>","wrap: WildcardHandler<E>","map: Record<string, string | undefined>","finalItem: CaptureInteractiveFindResponseItem","address: CleansingInternationalBatchServiceRequestAddress","map: OutputMap","request: VerifyServiceRequest","composed: VerifyComposedMatch[]","evaluation: \"pass\" | \"review\" | \"fail\"","params: {\n Origin?: string;\n Bias?: boolean;\n }","req: CaptureInteractiveFindRequest","params: {\n Origin?: string;\n }","request: NearByRequest","finalRequest: TypeAheadGeocodingRequest"],"sources":["../src/utils/deepStore.ts","../src/utils/avc/evaluator.ts","../src/utils/eventful.ts","../src/session/address/AddressSession.ts","../src/session/email/EmailSession.ts","../src/session/phone/PhoneSession.ts","../src/session/store-finder/StoreFinderSession.ts"],"sourcesContent":["// deepStore.ts\nexport type Unsubscribe = () => void;\nexport type Selector<T, S> = (state: T) => S;\nexport type EqualityFn<T> = (a: T, b: T) => boolean;\n\nexport type DeepStoreOptions = {\n arrayStrategy?: \"replace\" | \"concat\"; // how to merge arrays on patch()\n};\n\nexport type DeepPartial<T> = T extends Function\n ? T\n : T extends Array<infer U>\n ? Array<DeepPartial<U>>\n : T extends object\n ? { [K in keyof T]?: DeepPartial<T[K]> }\n : T;\n\nexport type DeepStore<T extends object> = {\n get(): T;\n set(next: T): void;\n patch(delta: DeepPartial<T>): void; // deep merge\n setAt(path: (string | number)[], value: any): void; // deep set\n reset(): void; // back to baseline\n markClean(): void; // make current = baseline\n isDirty(): boolean;\n subscribe(fn: () => void): Unsubscribe;\n subscribeSel<S>(sel: Selector<T, S>, eq?: EqualityFn<S>): Unsubscribe;\n};\n\nexport function createDeepStore<T extends object>(\n initial: T,\n opts: DeepStoreOptions = {},\n equals: EqualityFn<any> = deepEqual\n): DeepStore<T> {\n let value: T = clone(initial);\n let baseline: T = clone(initial);\n const subs = new Set<() => void>();\n const notify = () => subs.forEach((fn) => fn());\n\n const arrayStrategy = opts.arrayStrategy ?? \"replace\";\n\n const store: DeepStore<T> = {\n get: () => value,\n set: (next) => {\n value = clone(next);\n notify();\n },\n patch: (delta) => {\n value = deepMerge(value, delta as any, arrayStrategy);\n notify();\n },\n setAt: (path, v) => {\n value = setAtPath(value, path, v);\n notify();\n },\n reset: () => {\n value = clone(baseline);\n notify();\n },\n markClean: () => {\n baseline = clone(value);\n },\n isDirty: () => !equals(value, baseline),\n subscribe: (fn) => {\n subs.add(fn);\n return () => subs.delete(fn);\n },\n subscribeSel: (sel, eq = Object.is) => {\n let prev = sel(value);\n const listener = () => {\n const next = sel(value);\n if (!eq(prev, next)) {\n prev = next;\n fn();\n }\n };\n const fn = () => {}; // placeholder so we can return same unsub signature\n subs.add(listener);\n return () => subs.delete(listener);\n },\n };\n\n return store;\n}\n\n/* ----------------- helpers ----------------- */\n\nfunction isObj(x: any): x is object {\n return x !== null && typeof x === \"object\";\n}\nfunction isPlainObject(x: any) {\n if (!isObj(x)) return false;\n const proto = Object.getPrototypeOf(x);\n return proto === Object.prototype || proto === null;\n}\n\nfunction clone<T>(x: T): T {\n // shallow clone for plain objects/arrays; Dates copied by value\n if (Array.isArray(x)) return x.map(clone) as any;\n if (x instanceof Date) return new Date(x.getTime()) as any;\n if (isPlainObject(x)) {\n const out: any = {};\n for (const k in x as any) out[k] = clone((x as any)[k]);\n return out;\n }\n return x;\n}\n\nfunction deepEqual(a: any, b: any): boolean {\n if (Object.is(a, b)) return true;\n if (a instanceof Date && b instanceof Date)\n return a.getTime() === b.getTime();\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;\n return true;\n }\n if (isPlainObject(a) && isPlainObject(b)) {\n const ak = Object.keys(a),\n bk = Object.keys(b);\n if (ak.length !== bk.length) return false;\n for (const k of ak) if (!deepEqual(a[k], b[k])) return false;\n return true;\n }\n return false;\n}\n\nfunction deepMerge<T extends object>(\n base: T,\n delta: Partial<T>,\n arrayStrategy: \"replace\" | \"concat\"\n): T {\n if (!isPlainObject(base) || !isPlainObject(delta))\n return clone(delta as T) ?? clone(base);\n\n const out: any = Array.isArray(base)\n ? [...(base as any)]\n : { ...(base as any) };\n for (const k of Object.keys(delta as any)) {\n const bv = (base as any)[k];\n const dv = (delta as any)[k];\n\n if (dv === undefined) continue;\n if (Array.isArray(bv) && Array.isArray(dv)) {\n out[k] = arrayStrategy === \"concat\" ? [...bv, ...dv] : clone(dv);\n } else if (isPlainObject(bv) && isPlainObject(dv)) {\n out[k] = deepMerge(bv, dv, arrayStrategy);\n } else {\n out[k] = clone(dv);\n }\n }\n return out;\n}\n\nfunction setAtPath<T extends object>(\n obj: T,\n path: (string | number)[],\n value: any\n): T {\n if (!path.length) return clone(value);\n const out: any = Array.isArray(obj) ? [...(obj as any)] : { ...(obj as any) };\n\n let cur: any = out;\n for (let i = 0; i < path.length - 1; i++) {\n const key = path[i];\n const next = cur[key];\n if (Array.isArray(next)) cur[key] = [...next];\n else if (isPlainObject(next)) cur[key] = { ...next };\n else cur[key] = typeof path[i + 1] === \"number\" ? [] : {};\n cur = cur[key];\n }\n cur[path[path.length - 1]] = clone(value);\n return out;\n}\n","import {\n parseAVC,\n ParsedAVC,\n PARSING_STATUS_RANKING,\n VERIFICATION_STATUS_RANKING,\n} from \"./parser\";\n\nexport type ComparisonOperator = \">\" | \">=\" | \"<\" | \"<=\" | \"===\";\n\nexport type SymbolicTest = {\n [op in ComparisonOperator]?: string | number;\n};\n\nexport type AVCField =\n | \"matchscore\"\n | \"verificationStatus\"\n | \"postMatchLevel\"\n | \"preMatchLevel\"\n | \"parsingStatus\"\n | \"lexiconIdentificationMatchLevel\"\n | \"contextIdentificationMatchLevel\"\n | \"postcodeStatus\";\n\nexport type MatchRule = Partial<Record<AVCField, SymbolicTest>>;\n\nexport const DEFAULT_GREEN_TEST: MatchRule = {\n verificationStatus: { \"===\": \"V\" }, // Verified\n matchscore: { \">=\": 90 }, // Match score of 90 or higher\n};\nexport const DEFAULT_ORANGE_TEST: MatchRule = {\n verificationStatus: { \">=\": \"P\" }, // Verified\n matchscore: { \">=\": 80 }, // Match score of 80 or higher\n};\n\nexport const testAVC = (\n avc: ParsedAVC | string,\n rules: MatchRule = DEFAULT_GREEN_TEST\n): boolean => {\n if (typeof avc === \"string\") {\n avc = parseAVC(avc);\n }\n for (const [field, test] of Object.entries(rules) as [\n AVCField,\n SymbolicTest\n ][]) {\n const actualValue = getFieldNumericValue(field, avc);\n\n for (const [operator, expectedRaw] of Object.entries(test) as [\n ComparisonOperator,\n string\n ][]) {\n const expectedValue = resolveRank(field, expectedRaw);\n\n if (!evaluateOperator(actualValue, operator, expectedValue)) {\n return false; // Any test fails = full match fails\n }\n }\n }\n\n return true;\n};\n\nexport const getFieldNumericValue = (\n field: AVCField,\n avc: ParsedAVC\n): number => {\n switch (field) {\n case \"matchscore\":\n return avc.matchscore;\n case \"verificationStatus\":\n return resolveRank(field, avc.verificationStatus);\n case \"postMatchLevel\":\n return avc.postMatchLevel;\n case \"preMatchLevel\":\n return avc.preMatchLevel;\n case \"parsingStatus\":\n return resolveRank(field, avc.parsingStatus);\n case \"lexiconIdentificationMatchLevel\":\n return avc.lexiconIdentificationMatchLevel;\n case \"contextIdentificationMatchLevel\":\n return avc.contextIdentificationMatchLevel;\n case \"postcodeStatus\":\n return avc.postcodeStatusNumeric;\n }\n};\n\nexport const resolveRank = (field: AVCField, value: string): number => {\n switch (field) {\n case \"verificationStatus\":\n return VERIFICATION_STATUS_RANKING[value] ?? throwInvalid(field, value);\n case \"parsingStatus\":\n return PARSING_STATUS_RANKING[value] ?? throwInvalid(field, value);\n default:\n const num = Number(value);\n if (isNaN(num)) throwInvalid(field, value);\n return num;\n }\n};\n\nexport const throwInvalid = (f: string, v: string): never => {\n throw new Error(`Invalid symbolic value \"${v}\" for field \"${f}\"`);\n};\n\nexport const evaluateOperator = (\n a: number,\n operator: ComparisonOperator,\n b: number\n): boolean => {\n switch (operator) {\n case \">\":\n return a > b;\n case \">=\":\n return a >= b;\n case \"<\":\n return a < b;\n case \"<=\":\n return a <= b;\n case \"===\":\n return a === b;\n default:\n throw new Error(`Unsupported operator: ${operator}`);\n }\n};\n","// eventful.ts\nimport mitt from \"mitt\";\nimport type { Emitter, Handler, WildcardHandler } from \"mitt\";\n\nexport interface BaseLogger {\n debug: (...args: unknown[]) => void;\n info: (...args: unknown[]) => void;\n warn: (...args: unknown[]) => void;\n error: (...args: unknown[]) => void;\n}\nexport const noopLogger: BaseLogger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n};\n/**\n * Generic, typed event-emitter base with optional store + state snapshot\n * provided to *WithStore helpers.\n *\n * E: event map (key -> payload type)\n * S: state shape (your snapshot)\n * TStore: store interface you want to expose (can be read-only)\n */\nexport abstract class Eventful<\n E extends Record<string, any>,\n S,\n TStore,\n TLogger extends BaseLogger = BaseLogger\n> {\n protected readonly emitter: Emitter<E>;\n protected readonly logger: TLogger;\n\n protected constructor(logger?: TLogger) {\n this.emitter = mitt<E>();\n this.logger = logger || (noopLogger as TLogger);\n }\n\n /** Subclasses must provide a current snapshot for WithStore helpers. */\n protected abstract getState(): S;\n\n /** Subclasses must provide a store reference (or a facade) for WithStore helpers. */\n protected abstract getStore(): TStore;\n\n // -------- Core API --------\n public on<K extends keyof E>(type: K, handler: Handler<E[K]>): () => void {\n this.emitter.on(type, handler);\n return () => this.off(type, handler);\n }\n\n public off<K extends keyof E>(type: K, handler: Handler<E[K]>): void {\n this.emitter.off(type, handler);\n }\n\n public once<K extends keyof E>(type: K, handler: Handler<E[K]>): () => void {\n const wrap: Handler<E[K]> = (evt) => {\n this.emitter.off(type, wrap);\n handler(evt);\n };\n this.emitter.on(type, wrap);\n return () => this.emitter.off(type, wrap);\n }\n\n // -------- Wildcard --------\n public onAny(handler: WildcardHandler<E>): () => void {\n this.emitter.on(\"*\", handler);\n return () => this.offAny(handler);\n }\n\n public offAny(handler: WildcardHandler<E>): void {\n this.emitter.off(\"*\", handler);\n }\n\n public onceAny(handler: WildcardHandler<E>): () => void {\n const wrap: WildcardHandler<E> = (type, evt) => {\n this.emitter.off(\"*\", wrap);\n handler(type, evt);\n };\n this.emitter.on(\"*\", wrap);\n return () => this.emitter.off(\"*\", wrap);\n }\n\n // -------- WithStore helpers --------\n public onWithStore<K extends keyof E>(\n type: K,\n handler: (payload: E[K], ctx: { store: TStore; state: S }) => void\n ): () => void {\n const wrap = (payload: E[K]) =>\n handler(payload, { store: this.getStore(), state: this.getState() });\n this.emitter.on(type, wrap as any);\n return () => this.emitter.off(type, wrap as any);\n }\n\n public onceWithStore<K extends keyof E>(\n type: K,\n handler: (payload: E[K], ctx: { store: TStore; state: S }) => void\n ): () => void {\n const wrap = (payload: E[K]) => {\n this.emitter.off(type, wrap as any);\n handler(payload, { store: this.getStore(), state: this.getState() });\n };\n this.emitter.on(type, wrap as any);\n return () => this.emitter.off(type, wrap as any);\n }\n\n public onAnyWithStore(\n handler: (\n type: keyof E,\n payload: E[keyof E],\n ctx: { store: TStore; state: S }\n ) => void\n ): () => void {\n const wrap = (type: any, payload: any) =>\n handler(type, payload, {\n store: this.getStore(),\n state: this.getState(),\n });\n this.emitter.on(\"*\", wrap as any);\n return () => this.emitter.off(\"*\", wrap as any);\n }\n\n public onceAnyWithStore(\n handler: (\n type: keyof E,\n payload: E[keyof E],\n ctx: { store: TStore; state: S }\n ) => void\n ): () => void {\n const wrap = (type: any, payload: any) => {\n this.emitter.off(\"*\", wrap as any);\n handler(type, payload, {\n store: this.getStore(),\n state: this.getState(),\n });\n };\n this.emitter.on(\"*\", wrap as any);\n return () => this.emitter.off(\"*\", wrap as any);\n }\n\n /** Protected emit for subclasses. Keeps external API clean. */\n protected emit<K extends keyof E>(type: K, payload: E[K]): void {\n this.emitter.emit(type, payload);\n }\n}\n","import { Loqate } from \"@loqate/core\";\nimport { RequestOptions } from \"@loqate/core/lib/sdks.js\";\nimport {\n CaptureInteractiveFindResponseItem,\n CaptureInteractiveRetrieveResponseItem,\n CleansingInternationalBatchServiceRequestAddress,\n CleansingInternationalBatchServiceResponseItem,\n CleansingInternationalBatchServiceResponseItemMatch,\n} from \"@loqate/core/models\";\nimport {\n CaptureInteractiveFindRequest,\n CaptureInteractiveRetrieveRequest,\n VerifyServiceRequest,\n} from \"@loqate/core/models/operations\";\nimport { createDeepStore, DeepPartial, DeepStore } from \"../../utils/deepStore\";\nimport { render } from \"../../utils/handles\";\nimport {\n DEFAULT_GREEN_TEST,\n DEFAULT_ORANGE_TEST,\n MatchRule,\n testAVC,\n} from \"../../utils/avc/evaluator\";\nimport { BaseLogger, Eventful } from \"../../utils/eventful\";\nimport { parseAVC, ParsedAVC } from \"../../utils/avc/parser\";\n\nexport {\n type CaptureInteractiveFindResponseItem,\n type CaptureInteractiveRetrieveResponseItem,\n type CleansingInternationalBatchServiceRequestAddress,\n type CleansingInternationalBatchServiceResponseItem,\n type CleansingInternationalBatchServiceResponseItemMatch,\n} from \"@loqate/core/models\";\nexport {\n type CaptureInteractiveFindRequest,\n type CaptureInteractiveRetrieveRequest,\n type VerifyServiceRequest,\n} from \"@loqate/core/models/operations\";\n\nexport type VerifyComposedMatch = {\n match: CleansingInternationalBatchServiceResponseItemMatch | null;\n parsedAVC: ParsedAVC | null;\n evaluation: \"pass\" | \"review\" | \"fail\";\n};\n\n//#region Types\nexport type AddressSessionInit = {\n apiKey: string;\n logger?: BaseLogger;\n biasing?: {\n country?: string;\n location?: {\n latitude?: number;\n longitude?: number;\n };\n };\n capture?: {\n mappings?: CaptureAddressMapping[];\n find?: {\n debounceMs?: number;\n request?: Partial<CaptureInteractiveFindRequest>;\n };\n retrieve?: {\n request?: Partial<CaptureInteractiveRetrieveRequest>;\n };\n };\n verify?: {\n request?: Partial<VerifyServiceRequest>;\n outputMappings?: VerifyOutputAddressMapping[];\n inputMappings?: VerifyInputAddressMapping[];\n evalution?: {\n pass?: MatchRule;\n review?: MatchRule;\n };\n };\n};\n\nexport type OutputMap = Record<string, string | undefined>;\n\nexport type AddressSessionState = {\n findCount: number;\n apiKey: string;\n retrieveCount: number;\n verifyCount: number;\n isCleanCapture: boolean;\n biasing?: {\n country?: string;\n location?: {\n latitude?: number;\n longitude?: number;\n };\n };\n outMap?: {\n captureClean?: boolean;\n verifyClean?: boolean;\n map?: OutputMap;\n };\n verify: {\n request?: Omit<VerifyServiceRequest, \"addresses\">;\n response?: VerifyComposedMatch[];\n requestHistory: {\n request: VerifyServiceRequest;\n options?: RequestOptions;\n }[];\n outputMappings: VerifyOutputAddressMapping[];\n inputMappings: VerifyInputAddressMapping[];\n evaluation: {\n pass: MatchRule;\n review: MatchRule;\n };\n };\n capture: {\n retrieve: {\n request?: Omit<CaptureInteractiveRetrieveRequest, \"id\">;\n mappings: CaptureAddressMapping[];\n items?: CaptureInteractiveRetrieveResponseItem[];\n debounceMs?: number;\n requestHistory: {\n request: CaptureInteractiveRetrieveRequest;\n options?: RequestOptions;\n }[];\n };\n find: {\n request?: Omit<CaptureInteractiveFindRequest, \"text\" | \"container\">;\n currentContainer?: string;\n searchText: string;\n requestHistory: {\n request: CaptureInteractiveFindRequest;\n options?: RequestOptions;\n }[];\n items?: CaptureInteractiveFindResponseItem[];\n debounceMs?: number;\n };\n };\n};\n\nexport type CaptureAddressMapping = {\n id: string;\n field?: string | keyof CaptureInteractiveFindResponseItem;\n};\nexport type VerifyOutputAddressMapping = {\n id: string;\n field?: string | keyof CleansingInternationalBatchServiceResponseItemMatch;\n};\nexport type VerifyInputAddressMapping = {\n id: string;\n field?: keyof CleansingInternationalBatchServiceRequestAddress;\n};\n\nexport type AddressSessionStateStore = DeepStore<AddressSessionState>;\n//#endregion\n\n//#region Events\nexport type AddressSessionEvents = {\n // Bias changes\n \"bias:changed\": AddressSessionState[\"biasing\"];\n\n // Find\n \"find:request\": { request: CaptureInteractiveFindRequest };\n \"find:response\": {\n request: CaptureInteractiveFindRequest;\n items: CaptureInteractiveFindResponseItem[];\n };\n\n \"find:expandContainer\": string;\n\n // Retrieve\n \"retrieve:request\": { request: CaptureInteractiveRetrieveRequest };\n \"retrieve:response\": {\n request: CaptureInteractiveRetrieveRequest;\n items: CaptureInteractiveRetrieveResponseItem[];\n map: OutputMap | undefined;\n };\n\n // Verify\n \"verify:request\": { request: VerifyServiceRequest };\n \"verify:response\": {\n request: VerifyServiceRequest;\n raw: CleansingInternationalBatchServiceResponseItem[];\n composed: VerifyComposedMatch[];\n };\n\n // Out map + state\n \"map:updated\": OutputMap;\n \"state:changed\": AddressSessionState;\n\n // Errors\n error: unknown;\n};\n//#endregion\n\nexport class AddressSession extends Eventful<\n AddressSessionEvents,\n AddressSessionState,\n AddressSessionStateStore\n> {\n private _loqate: Loqate;\n private _store: AddressSessionStateStore;\n\n constructor(init: AddressSessionInit) {\n super(init.logger);\n this._loqate = new Loqate({\n apiKey: init.apiKey,\n });\n\n this._store = createDeepStore({\n findCount: 0,\n apiKey: init.apiKey,\n retrieveCount: 0,\n verifyCount: 0,\n isCleanCapture: false,\n biasing: {\n country: init.biasing?.country,\n location: {\n latitude: init.biasing?.location?.latitude,\n longitude: init.biasing?.location?.longitude,\n },\n },\n verify: {\n request: init.verify?.request,\n requestHistory: [],\n outputMappings: init.verify?.outputMappings ?? [],\n inputMappings: init.verify?.inputMappings ?? [],\n items: [],\n evaluation: {\n pass: init.verify?.evalution?.pass ?? DEFAULT_GREEN_TEST,\n review: init.verify?.evalution?.review ?? DEFAULT_ORANGE_TEST,\n },\n },\n capture: {\n retrieve: {\n request: init.capture?.retrieve?.request,\n requestHistory: [],\n mappings: init.capture?.mappings ?? [],\n },\n find: {\n request: init.capture?.find?.request,\n searchText: \"\",\n items: [],\n requestHistory: [],\n currentContainer: undefined,\n debounceMs: init.capture?.find?.debounceMs ?? 0,\n },\n },\n });\n this.emit(\"state:changed\", this._store.get());\n }\n\n // ---- required by Eventful ----\n protected getState(): AddressSessionState {\n return this._store.get();\n }\n protected getStore(): AddressSessionStateStore {\n return this._store;\n }\n\n // -----------------------\n // Internal helpers\n // -----------------------\n\n /** Patch store and emit a single 'state:changed'. */\n private _patch(next: DeepPartial<AddressSessionState>): void {\n this._store.patch(next);\n this.emit(\"state:changed\", this._store.get());\n }\n\n private _biasChanged(): void {\n this.emit(\"bias:changed\", this._store.get().biasing);\n }\n\n async biasToIP(ip?: string): Promise<void> {\n try {\n const res = await this._loqate.geocoding.ip2Country({\n ipAddress: ip,\n });\n this._patch({\n biasing: {\n country: res.items?.[0]?.country || undefined,\n },\n });\n this.logger.info(\"Biased to IP country:\", this._store.get().biasing);\n this._biasChanged();\n } catch (error) {\n this.logger.error(\"Failed to bias to IP country:\", error);\n this.emit(\"error\", error);\n throw error;\n }\n }\n\n biasToCountry(country: string): void {\n this._patch({\n biasing: {\n country,\n },\n });\n this.logger.info(\"Biased to country:\", this._store.get().biasing);\n this._biasChanged();\n }\n\n biasToLocation(lat: number, lon: number): void {\n this._patch({\n biasing: {\n location: {\n latitude: lat,\n longitude: lon,\n },\n },\n });\n this.logger.info(\"Biased to location:\", this._store.get().biasing);\n this._biasChanged();\n }\n\n subscribe<S>(\n sel: (s: AddressSessionState) => S,\n onChange: (slice: S, store: DeepStore<AddressSessionState>) => void,\n eq: (a: S, b: S) => boolean = Object.is\n ): () => void {\n let prev = sel(this._store.get());\n this.logger.debug(\"Initial slice:\", prev);\n return this._store.subscribe(() => {\n const next = sel(this._store.get());\n this.logger.debug(\"Next slice:\", next);\n if (!eq(prev, next)) {\n prev = next;\n onChange(next, this._store);\n }\n });\n }\n\n private _generateOutMap(\n response:\n | CaptureInteractiveFindResponseItem\n | CleansingInternationalBatchServiceResponseItemMatch,\n source: \"capture\" | \"verify\"\n ): OutputMap {\n const map: Record<string, string | undefined> = {};\n const mappings =\n source == \"capture\"\n ? this._store.get().capture.retrieve.mappings\n : this._store.get().verify.outputMappings;\n for (const m of mappings) {\n if (m.field && m.field in response) {\n // @ts-ignore\n map[m.id] = response[m.field];\n } else if (m.field) {\n map[m.id] = render(m.field, response, { escapeHtml: false });\n } else {\n map[m.id] = undefined;\n }\n }\n return map;\n }\n\n private _generateCaptureMappingMap(\n response: CaptureInteractiveFindResponseItem\n ): OutputMap {\n return this._generateOutMap(response, \"capture\");\n }\n\n private _generateVerifyMappingMap(\n response: CleansingInternationalBatchServiceResponseItemMatch\n ): OutputMap {\n return this._generateOutMap(response, \"verify\");\n }\n\n async select(\n item: CaptureInteractiveFindResponseItem | number\n ): Promise<void> {\n this.logger.debug(\"Selected item:\", item);\n let finalItem: CaptureInteractiveFindResponseItem;\n if (typeof item === \"number\") {\n const items = this._store.get().capture.find.items ?? [];\n if (item < 0 || item >= items.length) {\n throw new Error(`Item index ${item} is out of bounds`);\n }\n item = items[item];\n }\n finalItem = item;\n if (finalItem.type !== \"Address\") {\n await this.expandContainer(finalItem);\n return;\n } else {\n await this.retrieve(finalItem);\n }\n }\n\n async expandContainer(\n container: string | CaptureInteractiveFindResponseItem | number\n ): Promise<CaptureInteractiveFindResponseItem[]> {\n this.logger.debug(\"Expanding container:\", container);\n if (typeof container === \"number\") {\n const items = this._store.get().capture.find.items ?? [];\n if (container < 0 || container >= items.length) {\n throw new Error(`Container index ${container} is out of bounds`);\n }\n container = items[container];\n }\n if (typeof container === \"object\" && \"type\" in container) {\n if (container.type === \"Address\") {\n throw new Error(`Cannot expand an address item`);\n }\n container = container?.id ?? \"\";\n }\n this.emit(\"find:expandContainer\", container as string);\n this._patch({\n capture: {\n find: {\n currentContainer: container as string,\n },\n },\n });\n\n let lastRequest = this._store\n .get()\n .capture.find.requestHistory?.slice(-1)[0];\n\n this.logger.debug(\n \"Expanding container checking history:\",\n lastRequest,\n this._store.get().capture.find.requestHistory\n );\n\n if (lastRequest) {\n this.logger.debug(\"Expanding container:\", lastRequest);\n return await this.find(\n {\n ...lastRequest.request,\n container: container as string,\n },\n lastRequest.options as any\n );\n }\n return [];\n }\n\n updateMapFields(\n updates: {\n id: string;\n value: string;\n }[]\n ): OutputMap {\n const map = this._store.get().outMap?.map ?? {};\n for (const u of updates) {\n map[u.id] = u.value;\n }\n this._patch({\n outMap: {\n map,\n captureClean: false,\n verifyClean: false,\n },\n });\n this.emit(\"map:updated\", map);\n return map;\n }\n\n updateMapField(id: string, value: string): OutputMap {\n return this.updateMapFields([{ id, value }]);\n }\n\n private _generateVerifyInputAddress(\n countryOverride: string | undefined\n ): CleansingInternationalBatchServiceRequestAddress {\n const address: CleansingInternationalBatchServiceRequestAddress = {};\n const inputMappings = this._store.get().verify.inputMappings;\n const outMap = this._store.get().outMap?.map ?? {};\n this.logger.debug(\"Generating verify input address:\", {\n inputMappings,\n outMap,\n });\n for (const m of inputMappings) {\n if (m.field) {\n // @ts-ignore\n address[m.field] = outMap[m.id];\n }\n }\n if (countryOverride) {\n address.country = countryOverride;\n }\n this.logger.debug(\"Generated verify input address:\", address);\n return address;\n }\n\n acceptVerifyResponse(): OutputMap {\n if (this._store.get().verify.response?.length === 0) {\n throw new Error(\"No verify response to commit from\");\n }\n let map: OutputMap = this._generateVerifyMappingMap(\n this._store.get().verify?.response?.[0]?.match ?? {}\n );\n this._patch({\n outMap: {\n map,\n verifyClean: true,\n },\n });\n return map;\n }\n\n async verify(options?: { force?: boolean; country?: string }): Promise<{\n raw: CleansingInternationalBatchServiceResponseItem[];\n map?: OutputMap;\n composed: VerifyComposedMatch[];\n }> {\n try {\n const { force, country } = options ?? {};\n\n if (this._store.get().outMap?.captureClean && !force) {\n this.logger.debug(\n \"Skipping verify, capture data clean and map unchanged\"\n );\n return { raw: [], composed: [] };\n }\n const request: VerifyServiceRequest = {\n ...this._store.get().verify.request,\n addresses: [this._generateVerifyInputAddress(country)],\n };\n this.emit(\"verify:request\", { request });\n this.logger.debug(\"Verifying address with request:\", request);\n const res = await this._loqate.cleansing.batch(request);\n this.logger.debug(\"Verifying address with response:\", res);\n let composed: VerifyComposedMatch[] = (res?.[0]?.matches ?? []).map(\n (m) => {\n if (!m.avc || m.avc.trim().length === 0) {\n return { match: m, parsedAVC: null, evaluation: \"fail\" };\n }\n const parsedAVC = parseAVC(m.avc ?? \"\");\n let evaluation: \"pass\" | \"review\" | \"fail\" = \"fail\";\n if (testAVC(parsedAVC, this._store.get().verify.evaluation.pass)) {\n evaluation = \"pass\";\n } else if (\n testAVC(parsedAVC, this._store.get().verify.evaluation.review)\n ) {\n evaluation = \"review\";\n }\n return { match: m, parsedAVC, evaluation };\n }\n );\n\n this._patch({\n verifyCount: this._store.get().verifyCount + 1,\n verify: {\n response: composed,\n requestHistory: [\n ...(this._store.get().verify.requestHistory ?? []),\n { request, options: {} },\n ],\n },\n });\n const raw = res ?? [];\n this.emit(\"verify:response\", { request, raw, composed });\n return {\n raw: raw,\n composed: composed,\n };\n } catch (error) {\n this.logger.error(\"Verify request failed:\", error);\n this.emit(\"error\", error);\n throw error;\n }\n }\n\n async retrieve(\n request:\n | CaptureInteractiveRetrieveRequest\n | CaptureInteractiveFindResponseItem\n | string\n | number,\n options?: RequestOptions\n ): Promise<{\n raw: CaptureInteractiveRetrieveResponseItem[];\n map?: OutputMap;\n }> {\n try {\n if (typeof request === \"number\") {\n const items = this._store.get().capture.find.items ?? [];\n if (request < 0 || request >= items.length) {\n throw new Error(`Item index ${request} is out of bounds`);\n }\n request = items[request];\n }\n if (typeof request === \"string\") {\n request = { id: request };\n }\n if (typeof request === \"object\" && \"type\" in request) {\n // If it's a find response item, ensure it's an address with an id\n if (request.type !== \"Address\") {\n throw new Error(\n `Cannot retrieve non-address item of type \"${request.type}\"`\n );\n }\n if (!request.id) {\n throw new Error(`Cannot retrieve item with no id`);\n }\n request = { id: request.id };\n }\n request = {\n ...this._store.get().capture.retrieve.request,\n ...request,\n };\n this.logger.debug(\"Retrieving address with request:\", request);\n this.emit(\"retrieve:request\", {\n request: request as CaptureInteractiveRetrieveRequest,\n });\n\n let res = await this._loqate.capture.retrieve(\n request as CaptureInteractiveRetrieveRequest,\n options\n );\n this.logger.debug(\"Retrieving address with response:\", res);\n let map = this._generateCaptureMappingMap(res?.items?.[0] ?? {});\n this._patch({\n outMap: {\n map,\n captureClean: true,\n },\n retrieveCount: this._store.get().retrieveCount + 1,\n capture: {\n find: {\n items: [],\n },\n retrieve: {\n items: res.items ?? [],\n requestHistory: [\n ...((this._store.get().capture.retrieve.requestHistory ??\n []) as any[]),\n { request, options },\n ],\n },\n },\n });\n const items = res?.items ?? [];\n this.emit(\"map:updated\", map);\n this.emit(\"retrieve:response\", {\n request: request as CaptureInteractiveRetrieveRequest,\n items,\n map,\n });\n return {\n raw: items,\n map,\n };\n } catch (error) {\n this.logger.error(\"Retrieve request failed:\", error);\n this.emit(\"error\", error);\n throw error;\n }\n }\n\n private _generateBiasParams(): {\n Origin?: string;\n Bias?: boolean;\n } {\n const biasing = this._store.get().biasing;\n const params: {\n Origin?: string;\n Bias?: boolean;\n } = {};\n if (biasing?.country) {\n params.Bias = true;\n params.Origin = biasing.country;\n }\n if (\n biasing?.location?.latitude !== undefined &&\n biasing?.location?.longitude !== undefined\n ) {\n params.Bias = true;\n params.Origin = `${biasing.location.latitude},${biasing.location.longitude}`;\n }\n return params;\n }\n\n // ---- debounce internals ----\n private _findTimer: ReturnType<typeof setTimeout> | null = null;\n private _findPendingResolves: Array<\n (items: CaptureInteractiveFindResponseItem[]) => void\n > = [];\n private _findPendingRejects: Array<(err: unknown) => void> = [];\n\n /**\n * Single public find method:\n * - If debounceMs <= 0: runs immediately.\n * - If debounceMs > 0: debounces and coalesces callers.\n */\n async find(\n request: CaptureInteractiveFindRequest | string,\n options?: RequestOptions\n ): Promise<CaptureInteractiveFindResponseItem[]> {\n const delay = this._store.get().capture.find.debounceMs ?? 0;\n\n const reqObj =\n typeof request === \"string\" ? { text: request } : { ...request };\n\n // Keep UI state responsive to typing\n this._patch({\n capture: { find: { searchText: reqObj.text ?? \"\" } },\n });\n\n // Immediate path\n if (delay <= 0) {\n // If a debounce run was pending (perhaps debounceMs changed at runtime),\n // cancel it and reject its waiters so nothing hangs.\n if (this._findTimer) {\n clearTimeout(this._findTimer);\n this._findTimer = null;\n while (this._findPendingRejects.length) {\n this._findPendingRejects.shift()!(\n new Error(\"find() debounced call was superseded by immediate run\")\n );\n }\n this._findPendingResolves = [];\n }\n return this._doFind(reqObj, options);\n }\n\n // Debounced path: coalesce callers into a single run\n const p = new Promise<CaptureInteractiveFindResponseItem[]>(\n (resolve, reject) => {\n this._findPendingResolves.push(resolve);\n this._findPendingRejects.push(reject);\n }\n );\n\n if (this._findTimer) clearTimeout(this._findTimer);\n this._findTimer = setTimeout(async () => {\n this._findTimer = null;\n try {\n const items = await this._doFind(reqObj, options);\n while (this._findPendingResolves.length) {\n this._findPendingResolves.shift()!(items);\n }\n this._findPendingRejects = [];\n } catch (err) {\n while (this._findPendingRejects.length) {\n this._findPendingRejects.shift()!(err);\n }\n this._findPendingResolves = [];\n }\n }, delay);\n\n return p;\n }\n\n /**\n * Internal, immediate execution used by find().\n */\n private async _doFind(\n request: CaptureInteractiveFindRequest,\n options?: RequestOptions\n ): Promise<CaptureInteractiveFindResponseItem[]> {\n try {\n let req: CaptureInteractiveFindRequest = {\n ...this._store.get().capture.find.request,\n ...this._generateBiasParams(),\n ...request,\n container:\n request.container ?? this._store.get().capture.find.currentContainer,\n };\n this.emit(\"find:request\", { request: req });\n const res = await this._loqate.capture.find(req, options);\n\n const patch = {\n findCount: this._store.get().findCount + 1,\n capture: {\n find: {\n items: res?.items ?? [],\n requestHistory: [\n ...((this._store.get().capture.find.requestHistory ??\n []) as any[]),\n { request: req, options },\n ],\n },\n },\n };\n this._patch(patch);\n const items = res?.items ?? [];\n this.emit(\"find:response\", { request: req, items });\n return items;\n } catch (error) {\n this.logger.error(\"Find request failed:\", error);\n this.emit(\"error\", error);\n throw error;\n }\n }\n}\n","import {\n EmailValidationBatchValidateRequest,\n EmailValidationInteractiveValidateRequest,\n} from \"@loqate/core/models/operations\";\nimport { BaseLogger, Eventful } from \"../../utils/eventful\";\nimport {\n EmailValidationBatchValidateResponseItem,\n EmailValidationInteractiveValidateResponseItem,\n} from \"@loqate/core/models\";\nimport { RequestOptions } from \"@loqate/core/lib/sdks.js\";\nimport { createDeepStore, DeepPartial, DeepStore } from \"../../utils/deepStore\";\nimport { Loqate } from \"@loqate/core\";\n\nexport {\n type EmailValidationBatchValidateRequest,\n type EmailValidationInteractiveValidateRequest,\n} from \"@loqate/core/models/operations\";\n\nexport {\n type EmailValidationBatchValidateResponseItem,\n type EmailValidationInteractiveValidateResponseItem,\n} from \"@loqate/core/models\";\n\n//#region Types\nexport type EmailSessionInit = {\n apiKey: string;\n logger?: BaseLogger;\n validate?: {\n request?: Partial<EmailValidationInteractiveValidateRequest>;\n };\n batch?: {\n request?: Partial<EmailValidationBatchValidateRequest>;\n };\n};\n\nexport type EmailSessionState = {\n validateCount: number;\n batchCount: number;\n apiKey: string;\n validate: {\n email?: string;\n request?: Omit<EmailValidationInteractiveValidateRequest, \"email\">;\n response?: EmailValidationInteractiveValidateResponseItem[];\n requestHistory: {\n request: EmailValidationInteractiveValidateRequest;\n options?: RequestOptions;\n }[];\n };\n batch: {\n emails?: string[];\n request?: Omit<EmailValidationBatchValidateRequest, \"emails\">;\n response?: EmailValidationBatchValidateResponseItem[];\n requestHistory: {\n request: EmailValidationBatchValidateRequest;\n options?: RequestOptions;\n }[];\n };\n};\n\nexport type EmailSessionStateStore = DeepStore<EmailSessionState>;\n//#endregion\n\n//#region Events\nexport type EmailSessionEvents = {\n // validate\n \"validate:request\": { request: EmailValidationInteractiveValidateRequest };\n \"validate:response\": {\n request: EmailValidationInteractiveValidateRequest;\n response: EmailValidationInteractiveValidateResponseItem[];\n };\n\n // batch\n \"batch:request\": { request: EmailValidationBatchValidateRequest };\n \"batch:response\": {\n request: EmailValidationBatchValidateRequest;\n response: EmailValidationBatchValidateResponseItem[];\n };\n\n \"state:changed\": EmailSessionState;\n\n // Errors\n error: unknown;\n};\n//#endregion\n\nexport class EmailSession extends Eventful<\n EmailSessionEvents,\n EmailSessionState,\n EmailSessionStateStore\n> {\n private _loqate: Loqate;\n private _store: EmailSessionStateStore;\n\n constructor(init: EmailSessionInit) {\n super(init.logger);\n this._loqate = new Loqate({\n apiKey: init.apiKey,\n });\n\n this._store = createDeepStore({\n validateCount: 0,\n batchCount: 0,\n apiKey: init.apiKey,\n\n validate: {\n request: init.validate?.request,\n requestHistory: [],\n items: [],\n },\n batch: {\n request: init.batch?.request,\n requestHistory: [],\n items: [],\n },\n });\n this.emit(\"state:changed\", this._store.get());\n }\n\n // ---- required by Eventful ----\n protected getState(): EmailSessionState {\n return this._store.get();\n }\n protected getStore(): EmailSessionStateStore {\n return this._store;\n }\n\n // -----------------------\n // Internal helpers\n // -----------------------\n\n /** Patch store and emit a single 'state:changed'. */\n private _patch(next: DeepPartial<EmailSessionState>): void {\n this._store.patch(next);\n this.emit(\"state:changed\", this._store.get());\n }\n\n subscribe<S>(\n sel: (s: EmailSessionState) => S,\n onChange: (slice: S, store: DeepStore<EmailSessionState>) => void,\n eq: (a: S, b: S) => boolean = Object.is\n ): () => void {\n let prev = sel(this._store.get());\n this.logger.debug(\"Initial slice:\", prev);\n return this._store.subscribe(() => {\n const next = sel(this._store.get());\n this.logger.debug(\"Next slice:\", next);\n if (!eq(prev, next)) {\n prev = next;\n onChange(next, this._store);\n }\n });\n }\n\n async validate(\n request: EmailValidationInteractiveValidateRequest | string,\n options?: RequestOptions\n ): Promise<EmailValidationInteractiveValidateResponseItem[]> {\n try {\n if (typeof request === \"string\") {\n request = { email: request };\n }\n this._patch({\n validate: {\n email: request.email,\n },\n });\n request = {\n ...this._store.get().validate.request,\n ...request,\n };\n this.emit(\"validate:request\", { request });\n this.logger.debug(\"Validating email with request:\", request);\n let res = await this._loqate.emailValidation.validate(request, options);\n this.logger.debug(\"Validating email with response:\", res);\n this._patch({\n validateCount: this._store.get().validateCount + 1,\n validate: {\n response: res?.items ?? [],\n requestHistory: [\n ...((this._store.get().validate.requestHistory ?? []) as any[]),\n { request, options },\n ],\n },\n });\n const response = res?.items ?? [];\n this.emit(\"validate:response\", { request, response });\n return response;\n } catch (error) {\n this.logger.error(\"Validate request failed:\", error);\n this.emit(\"error\", error);\n throw error;\n }\n }\n\n async batch(\n request: EmailValidationBatchValidateRequest | string[] | string,\n options?: RequestOptions\n ): Promise<EmailValidationBatchValidateResponseItem[]> {\n try {\n if (typeof request === \"string\") {\n request = { emails: request };\n } else if (Array.isArray(request)) {\n request = { emails: request.join(\",\") };\n }\n this._patch({\n batch: {\n emails: (request as EmailValidationBatchValidateRequest).emails.split(\n \",\"\n ),\n },\n });\n request = {\n ...this._store.get().batch.request,\n ...request,\n };\n this.emit(\"batch:request\", { request });\n this.logger.debug(\"Validating emails with request:\", request);\n let res = await this._loqate.emailValidation.validateBatch(\n request,\n options\n );\n this.logger.debug(\"Validating emails with response:\", res);\n this._patch({\n batchCount: this._store.get().batchCount + 1,\n batch: {\n response: res?.items ?? [],\n requestHistory: [\n ...((this._store.get().batch.requestHistory ?? []) as any[]),\n { request, options },\n ],\n },\n });\n const response = res?.items ?? [];\n this.emit(\"batch:response\", { request, response });\n return response;\n } catch (error) {\n this.logger.error(\"Batch request failed:\", error);\n this.emit(\"error\", error);\n throw error;\n }\n }\n}\n","import { PhoneNumberValidationInteractiveValidateRequest } from \"@loqate/core/models/operations\";\nimport { BaseLogger, Eventful } from \"../../utils/eventful\";\nimport { PhoneNumberValidationInteractiveValidateResponseItem } from \"@loqate/core/models\";\nimport { RequestOptions } from \"@loqate/core/lib/sdks.js\";\nimport { createDeepStore, DeepPartial, DeepStore } from \"../../utils/deepStore\";\nimport { Loqate } from \"@loqate/core\";\n\nexport { type PhoneNumberValidationInteractiveValidateRequest } from \"@loqate/core/models/operations\";\nexport { type PhoneNumberValidationInteractiveValidateResponseItem } from \"@loqate/core/models\";\n\n//#region Types\nexport type PhoneSessionInit = {\n apiKey: string;\n logger?: BaseLogger;\n validate?: {\n request?: Partial<PhoneNumberValidationInteractiveValidateRequest>;\n };\n};\n\nexport type PhoneSessionState = {\n validateCount: number;\n apiKey: string;\n validate: {\n phoneNumber?: string;\n request?: Omit<PhoneNumberValidationInteractiveValidateRequest, \"phone\">;\n response?: PhoneNumberValidationInteractiveValidateResponseItem[];\n requestHistory: {\n request: PhoneNumberValidationInteractiveValidateRequest;\n options?: RequestOptions;\n }[];\n };\n};\n\nexport type PhoneSessionStateStore = DeepStore<PhoneSessionState>;\n//#endregion\n\n//#region Events\nexport type PhoneSessionEvents = {\n // validate\n \"validate:request\": {\n request: PhoneNumberValidationInteractiveValidateRequest;\n };\n \"validate:response\": {\n request: PhoneNumberValidationInteractiveValidateRequest;\n response: PhoneNumberValidationInteractiveValidateResponseItem[];\n };\n\n \"state:changed\": PhoneSessionState;\n\n // Errors\n error: unknown;\n};\n//#endregion\n\nexport class PhoneSession extends Eventful<\n PhoneSessionEvents,\n PhoneSessionState,\n PhoneSessionStateStore\n> {\n private _loqate: Loqate;\n private _store: PhoneSessionStateStore;\n\n constructor(init: PhoneSessionInit) {\n super(init.logger);\n this._loqate = new Loqate({\n apiKey: init.apiKey,\n });\n\n this._store = createDeepStore({\n validateCount: 0,\n apiKey: init.apiKey,\n validate: {\n request: init?.validate?.request,\n requestHistory: [],\n },\n });\n this.emit(\"state:changed\", this._store.get());\n }\n\n // ---- required by Eventful ----\n protected getState(): PhoneSessionState {\n return this._store.get();\n }\n protected getStore(): PhoneSessionStateStore {\n return this._store;\n }\n\n // -----------------------\n // Internal helpers\n // -----------------------\n\n /** Patch store and emit a single 'state:changed'. */\n private _patch(next: DeepPartial<PhoneSessionState>): void {\n this._store.patch(next);\n this.emit(\"state:changed\", this._store.get());\n }\n\n subscribe<S>(\n sel: (s: PhoneSessionState) => S,\n onChange: (slice: S, store: DeepStore<PhoneSessionState>) => void,\n eq: (a: S, b: S) => boolean = Object.is\n ): () => void {\n let prev = sel(this._store.get());\n this.logger.debug(\"Initial slice:\", prev);\n return this._store.subscribe(() => {\n const next = sel(this._store.get());\n this.logger.debug(\"Next slice:\", next);\n if (!eq(prev, next)) {\n prev = next;\n onChange(next, this._store);\n }\n });\n }\n\n async validate(\n request: PhoneNumberValidationInteractiveValidateRequest | string,\n options?: RequestOptions\n ): Promise<PhoneNumberValidationInteractiveValidateResponseItem[]> {\n try {\n if (typeof request === \"string\") {\n request = { phone: request };\n }\n this._patch({\n validate: {\n phoneNumber: request.phone,\n },\n });\n request = {\n ...this._store.get().validate.request,\n ...request,\n };\n this.emit(\"validate:request\", { request });\n this.logger.debug(\"Validating phone with request:\", request);\n let res = await this._loqate.phoneNumberValidation.validate(\n request,\n options\n );\n const response = res?.items ?? [];\n this.logger.debug(\"Validating phone with response:\", res);\n this._patch({\n validateCount: this._store.get().validateCount + 1,\n validate: {\n response,\n requestHistory: [\n ...((this._store.get().validate.requestHistory ?? []) as any[]),\n { request, options },\n ],\n },\n });\n\n this.emit(\"validate:response\", { request, response });\n return response;\n } catch (error) {\n this.logger.error(\"Validate request failed:\", error);\n this.emit(\"error\", error);\n throw error;\n }\n }\n}\n","import { Loqate } from \"@loqate/core\";\nimport { createDeepStore, DeepPartial, DeepStore } from \"../../utils/deepStore\";\nimport { BaseLogger, Eventful } from \"../../utils/eventful\";\nimport {\n ExternalResponse,\n NearByRequest,\n TypeAheadGeocodingRequest,\n} from \"@loqate/core/models/operations\";\nimport { RequestOptions } from \"@loqate/core/lib/sdks.js\";\nimport {\n LocationServicesDistanceFinderNearbyExternalResponseDestinationLocation,\n LocationServicesDistanceFinderNearbyPointData,\n LocationServicesGeocodingGlobalTypeAheadExternalTypeaheadResponseItem,\n} from \"@loqate/core/models\";\nimport { T } from \"vitest/dist/chunks/environment.LoooBwUu.js\";\n\nexport {\n type ExternalRe