@loqate/compose
Version:
Loqate Compose SDK — high-level flows that compose base API clients into easy-to-use sessions.
1,106 lines (1,098 loc) • 33.1 kB
JavaScript
import { PARSING_STATUS_RANKING, VERIFICATION_STATUS_RANKING, parseAVC, render } from "./parser-DXBc9ZXL.js";
import { Loqate } from "@loqate/core";
import mitt from "mitt";
//#region src/utils/deepStore.ts
function createDeepStore(initial, opts = {}, equals = deepEqual) {
let value = clone(initial);
let baseline = clone(initial);
const subs = /* @__PURE__ */ new Set();
const notify = () => subs.forEach((fn) => fn());
const arrayStrategy = opts.arrayStrategy ?? "replace";
return {
get: () => value,
set: (next) => {
value = clone(next);
notify();
},
patch: (delta) => {
value = deepMerge(value, delta, arrayStrategy);
notify();
},
setAt: (path, v) => {
value = setAtPath(value, path, v);
notify();
},
reset: () => {
value = clone(baseline);
notify();
},
markClean: () => {
baseline = clone(value);
},
isDirty: () => !equals(value, baseline),
subscribe: (fn) => {
subs.add(fn);
return () => subs.delete(fn);
},
subscribeSel: (sel, eq = Object.is) => {
let prev = sel(value);
const listener = () => {
const next = sel(value);
if (!eq(prev, next)) {
prev = next;
fn();
}
};
const fn = () => {};
subs.add(listener);
return () => subs.delete(listener);
}
};
}
function isObj(x) {
return x !== null && typeof x === "object";
}
function isPlainObject(x) {
if (!isObj(x)) return false;
const proto = Object.getPrototypeOf(x);
return proto === Object.prototype || proto === null;
}
function clone(x) {
if (Array.isArray(x)) return x.map(clone);
if (x instanceof Date) return new Date(x.getTime());
if (isPlainObject(x)) {
const out = {};
for (const k in x) out[k] = clone(x[k]);
return out;
}
return x;
}
function deepEqual(a, b) {
if (Object.is(a, b)) return true;
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
return true;
}
if (isPlainObject(a) && isPlainObject(b)) {
const ak = Object.keys(a), bk = Object.keys(b);
if (ak.length !== bk.length) return false;
for (const k of ak) if (!deepEqual(a[k], b[k])) return false;
return true;
}
return false;
}
function deepMerge(base, delta, arrayStrategy) {
if (!isPlainObject(base) || !isPlainObject(delta)) return clone(delta) ?? clone(base);
const out = Array.isArray(base) ? [...base] : { ...base };
for (const k of Object.keys(delta)) {
const bv = base[k];
const dv = delta[k];
if (dv === void 0) continue;
if (Array.isArray(bv) && Array.isArray(dv)) out[k] = arrayStrategy === "concat" ? [...bv, ...dv] : clone(dv);
else if (isPlainObject(bv) && isPlainObject(dv)) out[k] = deepMerge(bv, dv, arrayStrategy);
else out[k] = clone(dv);
}
return out;
}
function setAtPath(obj, path, value) {
if (!path.length) return clone(value);
const out = Array.isArray(obj) ? [...obj] : { ...obj };
let cur = out;
for (let i = 0; i < path.length - 1; i++) {
const key = path[i];
const next = cur[key];
if (Array.isArray(next)) cur[key] = [...next];
else if (isPlainObject(next)) cur[key] = { ...next };
else cur[key] = typeof path[i + 1] === "number" ? [] : {};
cur = cur[key];
}
cur[path[path.length - 1]] = clone(value);
return out;
}
//#endregion
//#region src/utils/avc/evaluator.ts
const DEFAULT_GREEN_TEST = {
verificationStatus: { "===": "V" },
matchscore: { ">=": 90 }
};
const DEFAULT_ORANGE_TEST = {
verificationStatus: { ">=": "P" },
matchscore: { ">=": 80 }
};
const testAVC = (avc, rules = DEFAULT_GREEN_TEST) => {
if (typeof avc === "string") avc = parseAVC(avc);
for (const [field, test] of Object.entries(rules)) {
const actualValue = getFieldNumericValue(field, avc);
for (const [operator, expectedRaw] of Object.entries(test)) if (!evaluateOperator(actualValue, operator, resolveRank(field, expectedRaw))) return false;
}
return true;
};
const getFieldNumericValue = (field, avc) => {
switch (field) {
case "matchscore": return avc.matchscore;
case "verificationStatus": return resolveRank(field, avc.verificationStatus);
case "postMatchLevel": return avc.postMatchLevel;
case "preMatchLevel": return avc.preMatchLevel;
case "parsingStatus": return resolveRank(field, avc.parsingStatus);
case "lexiconIdentificationMatchLevel": return avc.lexiconIdentificationMatchLevel;
case "contextIdentificationMatchLevel": return avc.contextIdentificationMatchLevel;
case "postcodeStatus": return avc.postcodeStatusNumeric;
}
};
const resolveRank = (field, value) => {
switch (field) {
case "verificationStatus": return VERIFICATION_STATUS_RANKING[value] ?? throwInvalid(field, value);
case "parsingStatus": return PARSING_STATUS_RANKING[value] ?? throwInvalid(field, value);
default:
const num = Number(value);
if (isNaN(num)) throwInvalid(field, value);
return num;
}
};
const throwInvalid = (f, v) => {
throw new Error(`Invalid symbolic value "${v}" for field "${f}"`);
};
const evaluateOperator = (a, operator, b) => {
switch (operator) {
case ">": return a > b;
case ">=": return a >= b;
case "<": return a < b;
case "<=": return a <= b;
case "===": return a === b;
default: throw new Error(`Unsupported operator: ${operator}`);
}
};
//#endregion
//#region src/utils/eventful.ts
const noopLogger = {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {}
};
/**
* Generic, typed event-emitter base with optional store + state snapshot
* provided to *WithStore helpers.
*
* E: event map (key -> payload type)
* S: state shape (your snapshot)
* TStore: store interface you want to expose (can be read-only)
*/
var Eventful = class {
constructor(logger) {
this.emitter = mitt();
this.logger = logger || noopLogger;
}
on(type, handler) {
this.emitter.on(type, handler);
return () => this.off(type, handler);
}
off(type, handler) {
this.emitter.off(type, handler);
}
once(type, handler) {
const wrap = (evt) => {
this.emitter.off(type, wrap);
handler(evt);
};
this.emitter.on(type, wrap);
return () => this.emitter.off(type, wrap);
}
onAny(handler) {
this.emitter.on("*", handler);
return () => this.offAny(handler);
}
offAny(handler) {
this.emitter.off("*", handler);
}
onceAny(handler) {
const wrap = (type, evt) => {
this.emitter.off("*", wrap);
handler(type, evt);
};
this.emitter.on("*", wrap);
return () => this.emitter.off("*", wrap);
}
onWithStore(type, handler) {
const wrap = (payload) => handler(payload, {
store: this.getStore(),
state: this.getState()
});
this.emitter.on(type, wrap);
return () => this.emitter.off(type, wrap);
}
onceWithStore(type, handler) {
const wrap = (payload) => {
this.emitter.off(type, wrap);
handler(payload, {
store: this.getStore(),
state: this.getState()
});
};
this.emitter.on(type, wrap);
return () => this.emitter.off(type, wrap);
}
onAnyWithStore(handler) {
const wrap = (type, payload) => handler(type, payload, {
store: this.getStore(),
state: this.getState()
});
this.emitter.on("*", wrap);
return () => this.emitter.off("*", wrap);
}
onceAnyWithStore(handler) {
const wrap = (type, payload) => {
this.emitter.off("*", wrap);
handler(type, payload, {
store: this.getStore(),
state: this.getState()
});
};
this.emitter.on("*", wrap);
return () => this.emitter.off("*", wrap);
}
/** Protected emit for subclasses. Keeps external API clean. */
emit(type, payload) {
this.emitter.emit(type, payload);
}
};
//#endregion
//#region src/session/address/AddressSession.ts
var AddressSession = class extends Eventful {
constructor(init) {
super(init.logger);
this._findTimer = null;
this._findPendingResolves = [];
this._findPendingRejects = [];
this._loqate = new Loqate({ apiKey: init.apiKey });
this._store = createDeepStore({
findCount: 0,
apiKey: init.apiKey,
retrieveCount: 0,
verifyCount: 0,
isCleanCapture: false,
biasing: {
country: init.biasing?.country,
location: {
latitude: init.biasing?.location?.latitude,
longitude: init.biasing?.location?.longitude
}
},
verify: {
request: init.verify?.request,
requestHistory: [],
outputMappings: init.verify?.outputMappings ?? [],
inputMappings: init.verify?.inputMappings ?? [],
items: [],
evaluation: {
pass: init.verify?.evalution?.pass ?? DEFAULT_GREEN_TEST,
review: init.verify?.evalution?.review ?? DEFAULT_ORANGE_TEST
}
},
capture: {
retrieve: {
request: init.capture?.retrieve?.request,
requestHistory: [],
mappings: init.capture?.mappings ?? []
},
find: {
request: init.capture?.find?.request,
searchText: "",
items: [],
requestHistory: [],
currentContainer: void 0,
debounceMs: init.capture?.find?.debounceMs ?? 0
}
}
});
this.emit("state:changed", this._store.get());
}
getState() {
return this._store.get();
}
getStore() {
return this._store;
}
/** Patch store and emit a single 'state:changed'. */
_patch(next) {
this._store.patch(next);
this.emit("state:changed", this._store.get());
}
_biasChanged() {
this.emit("bias:changed", this._store.get().biasing);
}
async biasToIP(ip) {
try {
const res = await this._loqate.geocoding.ip2Country({ ipAddress: ip });
this._patch({ biasing: { country: res.items?.[0]?.country || void 0 } });
this.logger.info("Biased to IP country:", this._store.get().biasing);
this._biasChanged();
} catch (error) {
this.logger.error("Failed to bias to IP country:", error);
this.emit("error", error);
throw error;
}
}
biasToCountry(country) {
this._patch({ biasing: { country } });
this.logger.info("Biased to country:", this._store.get().biasing);
this._biasChanged();
}
biasToLocation(lat, lon) {
this._patch({ biasing: { location: {
latitude: lat,
longitude: lon
} } });
this.logger.info("Biased to location:", this._store.get().biasing);
this._biasChanged();
}
subscribe(sel, onChange, eq = Object.is) {
let prev = sel(this._store.get());
this.logger.debug("Initial slice:", prev);
return this._store.subscribe(() => {
const next = sel(this._store.get());
this.logger.debug("Next slice:", next);
if (!eq(prev, next)) {
prev = next;
onChange(next, this._store);
}
});
}
_generateOutMap(response, source) {
const map = {};
const mappings = source == "capture" ? this._store.get().capture.retrieve.mappings : this._store.get().verify.outputMappings;
for (const m of mappings) if (m.field && m.field in response) map[m.id] = response[m.field];
else if (m.field) map[m.id] = render(m.field, response, { escapeHtml: false });
else map[m.id] = void 0;
return map;
}
_generateCaptureMappingMap(response) {
return this._generateOutMap(response, "capture");
}
_generateVerifyMappingMap(response) {
return this._generateOutMap(response, "verify");
}
async select(item) {
this.logger.debug("Selected item:", item);
let finalItem;
if (typeof item === "number") {
const items = this._store.get().capture.find.items ?? [];
if (item < 0 || item >= items.length) throw new Error(`Item index ${item} is out of bounds`);
item = items[item];
}
finalItem = item;
if (finalItem.type !== "Address") {
await this.expandContainer(finalItem);
return;
} else await this.retrieve(finalItem);
}
async expandContainer(container) {
this.logger.debug("Expanding container:", container);
if (typeof container === "number") {
const items = this._store.get().capture.find.items ?? [];
if (container < 0 || container >= items.length) throw new Error(`Container index ${container} is out of bounds`);
container = items[container];
}
if (typeof container === "object" && "type" in container) {
if (container.type === "Address") throw new Error(`Cannot expand an address item`);
container = container?.id ?? "";
}
this.emit("find:expandContainer", container);
this._patch({ capture: { find: { currentContainer: container } } });
let lastRequest = this._store.get().capture.find.requestHistory?.slice(-1)[0];
this.logger.debug("Expanding container checking history:", lastRequest, this._store.get().capture.find.requestHistory);
if (lastRequest) {
this.logger.debug("Expanding container:", lastRequest);
return await this.find({
...lastRequest.request,
container
}, lastRequest.options);
}
return [];
}
updateMapFields(updates) {
const map = this._store.get().outMap?.map ?? {};
for (const u of updates) map[u.id] = u.value;
this._patch({ outMap: {
map,
captureClean: false,
verifyClean: false
} });
this.emit("map:updated", map);
return map;
}
updateMapField(id, value) {
return this.updateMapFields([{
id,
value
}]);
}
_generateVerifyInputAddress(countryOverride) {
const address = {};
const inputMappings = this._store.get().verify.inputMappings;
const outMap = this._store.get().outMap?.map ?? {};
this.logger.debug("Generating verify input address:", {
inputMappings,
outMap
});
for (const m of inputMappings) if (m.field) address[m.field] = outMap[m.id];
if (countryOverride) address.country = countryOverride;
this.logger.debug("Generated verify input address:", address);
return address;
}
acceptVerifyResponse() {
if (this._store.get().verify.response?.length === 0) throw new Error("No verify response to commit from");
let map = this._generateVerifyMappingMap(this._store.get().verify?.response?.[0]?.match ?? {});
this._patch({ outMap: {
map,
verifyClean: true
} });
return map;
}
async verify(options) {
try {
const { force, country } = options ?? {};
if (this._store.get().outMap?.captureClean && !force) {
this.logger.debug("Skipping verify, capture data clean and map unchanged");
return {
raw: [],
composed: []
};
}
const request = {
...this._store.get().verify.request,
addresses: [this._generateVerifyInputAddress(country)]
};
this.emit("verify:request", { request });
this.logger.debug("Verifying address with request:", request);
const res = await this._loqate.cleansing.batch(request);
this.logger.debug("Verifying address with response:", res);
let composed = (res?.[0]?.matches ?? []).map((m) => {
if (!m.avc || m.avc.trim().length === 0) return {
match: m,
parsedAVC: null,
evaluation: "fail"
};
const parsedAVC = parseAVC(m.avc ?? "");
let evaluation = "fail";
if (testAVC(parsedAVC, this._store.get().verify.evaluation.pass)) evaluation = "pass";
else if (testAVC(parsedAVC, this._store.get().verify.evaluation.review)) evaluation = "review";
return {
match: m,
parsedAVC,
evaluation
};
});
this._patch({
verifyCount: this._store.get().verifyCount + 1,
verify: {
response: composed,
requestHistory: [...this._store.get().verify.requestHistory ?? [], {
request,
options: {}
}]
}
});
const raw = res ?? [];
this.emit("verify:response", {
request,
raw,
composed
});
return {
raw,
composed
};
} catch (error) {
this.logger.error("Verify request failed:", error);
this.emit("error", error);
throw error;
}
}
async retrieve(request, options) {
try {
if (typeof request === "number") {
const items$1 = this._store.get().capture.find.items ?? [];
if (request < 0 || request >= items$1.length) throw new Error(`Item index ${request} is out of bounds`);
request = items$1[request];
}
if (typeof request === "string") request = { id: request };
if (typeof request === "object" && "type" in request) {
if (request.type !== "Address") throw new Error(`Cannot retrieve non-address item of type "${request.type}"`);
if (!request.id) throw new Error(`Cannot retrieve item with no id`);
request = { id: request.id };
}
request = {
...this._store.get().capture.retrieve.request,
...request
};
this.logger.debug("Retrieving address with request:", request);
this.emit("retrieve:request", { request });
let res = await this._loqate.capture.retrieve(request, options);
this.logger.debug("Retrieving address with response:", res);
let map = this._generateCaptureMappingMap(res?.items?.[0] ?? {});
this._patch({
outMap: {
map,
captureClean: true
},
retrieveCount: this._store.get().retrieveCount + 1,
capture: {
find: { items: [] },
retrieve: {
items: res.items ?? [],
requestHistory: [...this._store.get().capture.retrieve.requestHistory ?? [], {
request,
options
}]
}
}
});
const items = res?.items ?? [];
this.emit("map:updated", map);
this.emit("retrieve:response", {
request,
items,
map
});
return {
raw: items,
map
};
} catch (error) {
this.logger.error("Retrieve request failed:", error);
this.emit("error", error);
throw error;
}
}
_generateBiasParams() {
const biasing = this._store.get().biasing;
const params = {};
if (biasing?.country) {
params.Bias = true;
params.Origin = biasing.country;
}
if (biasing?.location?.latitude !== void 0 && biasing?.location?.longitude !== void 0) {
params.Bias = true;
params.Origin = `${biasing.location.latitude},${biasing.location.longitude}`;
}
return params;
}
/**
* Single public find method:
* - If debounceMs <= 0: runs immediately.
* - If debounceMs > 0: debounces and coalesces callers.
*/
async find(request, options) {
const delay = this._store.get().capture.find.debounceMs ?? 0;
const reqObj = typeof request === "string" ? { text: request } : { ...request };
this._patch({ capture: { find: { searchText: reqObj.text ?? "" } } });
if (delay <= 0) {
if (this._findTimer) {
clearTimeout(this._findTimer);
this._findTimer = null;
while (this._findPendingRejects.length) this._findPendingRejects.shift()(/* @__PURE__ */ new Error("find() debounced call was superseded by immediate run"));
this._findPendingResolves = [];
}
return this._doFind(reqObj, options);
}
const p = new Promise((resolve, reject) => {
this._findPendingResolves.push(resolve);
this._findPendingRejects.push(reject);
});
if (this._findTimer) clearTimeout(this._findTimer);
this._findTimer = setTimeout(async () => {
this._findTimer = null;
try {
const items = await this._doFind(reqObj, options);
while (this._findPendingResolves.length) this._findPendingResolves.shift()(items);
this._findPendingRejects = [];
} catch (err) {
while (this._findPendingRejects.length) this._findPendingRejects.shift()(err);
this._findPendingResolves = [];
}
}, delay);
return p;
}
/**
* Internal, immediate execution used by find().
*/
async _doFind(request, options) {
try {
let req = {
...this._store.get().capture.find.request,
...this._generateBiasParams(),
...request,
container: request.container ?? this._store.get().capture.find.currentContainer
};
this.emit("find:request", { request: req });
const res = await this._loqate.capture.find(req, options);
const patch = {
findCount: this._store.get().findCount + 1,
capture: { find: {
items: res?.items ?? [],
requestHistory: [...this._store.get().capture.find.requestHistory ?? [], {
request: req,
options
}]
} }
};
this._patch(patch);
const items = res?.items ?? [];
this.emit("find:response", {
request: req,
items
});
return items;
} catch (error) {
this.logger.error("Find request failed:", error);
this.emit("error", error);
throw error;
}
}
};
//#endregion
//#region src/session/email/EmailSession.ts
var EmailSession = class extends Eventful {
constructor(init) {
super(init.logger);
this._loqate = new Loqate({ apiKey: init.apiKey });
this._store = createDeepStore({
validateCount: 0,
batchCount: 0,
apiKey: init.apiKey,
validate: {
request: init.validate?.request,
requestHistory: [],
items: []
},
batch: {
request: init.batch?.request,
requestHistory: [],
items: []
}
});
this.emit("state:changed", this._store.get());
}
getState() {
return this._store.get();
}
getStore() {
return this._store;
}
/** Patch store and emit a single 'state:changed'. */
_patch(next) {
this._store.patch(next);
this.emit("state:changed", this._store.get());
}
subscribe(sel, onChange, eq = Object.is) {
let prev = sel(this._store.get());
this.logger.debug("Initial slice:", prev);
return this._store.subscribe(() => {
const next = sel(this._store.get());
this.logger.debug("Next slice:", next);
if (!eq(prev, next)) {
prev = next;
onChange(next, this._store);
}
});
}
async validate(request, options) {
try {
if (typeof request === "string") request = { email: request };
this._patch({ validate: { email: request.email } });
request = {
...this._store.get().validate.request,
...request
};
this.emit("validate:request", { request });
this.logger.debug("Validating email with request:", request);
let res = await this._loqate.emailValidation.validate(request, options);
this.logger.debug("Validating email with response:", res);
this._patch({
validateCount: this._store.get().validateCount + 1,
validate: {
response: res?.items ?? [],
requestHistory: [...this._store.get().validate.requestHistory ?? [], {
request,
options
}]
}
});
const response = res?.items ?? [];
this.emit("validate:response", {
request,
response
});
return response;
} catch (error) {
this.logger.error("Validate request failed:", error);
this.emit("error", error);
throw error;
}
}
async batch(request, options) {
try {
if (typeof request === "string") request = { emails: request };
else if (Array.isArray(request)) request = { emails: request.join(",") };
this._patch({ batch: { emails: request.emails.split(",") } });
request = {
...this._store.get().batch.request,
...request
};
this.emit("batch:request", { request });
this.logger.debug("Validating emails with request:", request);
let res = await this._loqate.emailValidation.validateBatch(request, options);
this.logger.debug("Validating emails with response:", res);
this._patch({
batchCount: this._store.get().batchCount + 1,
batch: {
response: res?.items ?? [],
requestHistory: [...this._store.get().batch.requestHistory ?? [], {
request,
options
}]
}
});
const response = res?.items ?? [];
this.emit("batch:response", {
request,
response
});
return response;
} catch (error) {
this.logger.error("Batch request failed:", error);
this.emit("error", error);
throw error;
}
}
};
//#endregion
//#region src/session/phone/PhoneSession.ts
var PhoneSession = class extends Eventful {
constructor(init) {
super(init.logger);
this._loqate = new Loqate({ apiKey: init.apiKey });
this._store = createDeepStore({
validateCount: 0,
apiKey: init.apiKey,
validate: {
request: init?.validate?.request,
requestHistory: []
}
});
this.emit("state:changed", this._store.get());
}
getState() {
return this._store.get();
}
getStore() {
return this._store;
}
/** Patch store and emit a single 'state:changed'. */
_patch(next) {
this._store.patch(next);
this.emit("state:changed", this._store.get());
}
subscribe(sel, onChange, eq = Object.is) {
let prev = sel(this._store.get());
this.logger.debug("Initial slice:", prev);
return this._store.subscribe(() => {
const next = sel(this._store.get());
this.logger.debug("Next slice:", next);
if (!eq(prev, next)) {
prev = next;
onChange(next, this._store);
}
});
}
async validate(request, options) {
try {
if (typeof request === "string") request = { phone: request };
this._patch({ validate: { phoneNumber: request.phone } });
request = {
...this._store.get().validate.request,
...request
};
this.emit("validate:request", { request });
this.logger.debug("Validating phone with request:", request);
let res = await this._loqate.phoneNumberValidation.validate(request, options);
const response = res?.items ?? [];
this.logger.debug("Validating phone with response:", res);
this._patch({
validateCount: this._store.get().validateCount + 1,
validate: {
response,
requestHistory: [...this._store.get().validate.requestHistory ?? [], {
request,
options
}]
}
});
this.emit("validate:response", {
request,
response
});
return response;
} catch (error) {
this.logger.error("Validate request failed:", error);
this.emit("error", error);
throw error;
}
}
};
//#endregion
//#region src/session/store-finder/StoreFinderSession.ts
var StoreFinderSession = class extends Eventful {
constructor(init) {
super(init.logger);
this._loqate = new Loqate({ apiKey: init.apiKey });
this._store = createDeepStore({
apiKey: init.apiKey,
selectedLocation: init.selectedLocation ?? void 0,
nearby: {
count: 0,
request: init?.nearby?.request,
requestHistory: []
},
typeahead: {
count: 0,
find: {
count: 0,
request: init?.typeahead?.find?.request,
requestHistory: []
},
retrieve: {
count: 0,
request: init?.typeahead?.retrieve?.request,
requestHistory: []
}
},
biasing: {
country: init.biasing?.country,
location: {
latitude: init.biasing?.location?.latitude,
longitude: init.biasing?.location?.longitude
}
}
});
this.emit("state:changed", this._store.get());
}
getState() {
return this._store.get();
}
getStore() {
return this._store;
}
/** Patch store and emit a single 'state:changed'. */
_patch(next) {
this._store.patch(next);
this.emit("state:changed", this._store.get());
}
_biasChanged() {
this.emit("bias:changed", this._store.get().biasing);
}
_locationChanged() {
this.emit("location:changed", this._store.get().selectedLocation);
}
_locationListChanged() {
this.emit("locationList:changed", this._store.get().nearby?.request ?? {});
}
async biasToIP(ip) {
try {
const res = await this._loqate.geocoding.ip2Country({ ipAddress: ip });
this._patch({ biasing: { country: res.items?.[0]?.country || void 0 } });
this.logger.info("Biased to IP country:", this._store.get().biasing);
this._biasChanged();
} catch (error) {
this.logger.error("Failed to bias to IP country:", error);
this.emit("error", error);
throw error;
}
}
biasToCountry(country) {
this._patch({ biasing: { country } });
this.logger.info("Biased to country:", this._store.get().biasing);
this._biasChanged();
}
biasToLocation(lat, lon) {
this._patch({ biasing: { location: {
latitude: lat,
longitude: lon
} } });
this.logger.info("Biased to location:", this._store.get().biasing);
this._biasChanged();
}
subscribe(sel, onChange, eq = Object.is) {
let prev = sel(this._store.get());
this.logger.debug("Initial slice:", prev);
return this._store.subscribe(() => {
const next = sel(this._store.get());
this.logger.debug("Next slice:", next);
if (!eq(prev, next)) {
prev = next;
onChange(next, this._store);
}
});
}
_generateBiasParams() {
const biasing = this._store.get().biasing;
const params = {};
if (biasing?.country) params.Origin = biasing.country;
if (biasing?.location?.latitude !== void 0 && biasing?.location?.longitude !== void 0) params.Origin = `${biasing.location.latitude},${biasing.location.longitude}`;
return params;
}
updateLocation(latitude, longitude) {
this._patch({ selectedLocation: {
latitude,
longitude
} });
this.logger.info("Updated selected location:", this._store.get().selectedLocation);
this._locationChanged();
}
updateLocationList(args) {
this._patch({ nearby: { request: {
...this._store.get().nearby?.request,
...args
} } });
this._locationListChanged();
this.logger.info("Updated nearby location list ID:", this._store.get().nearby?.request?.locationListId);
}
async nearby() {
try {
if (!this._store.get().selectedLocation) throw new Error("Cannot perform nearby search without a selected location");
if (!this._store.get().nearby?.request?.locationListId && !this._store.get().nearby?.request?.locations) throw new Error("Cannot perform nearby search without a locationListId or locations in the request");
const request = {
...this._store.get().nearby?.request,
originLocation: {
id: "SelectedLocation",
latitude: this._store.get().selectedLocation.latitude.toString(),
longitude: this._store.get().selectedLocation.longitude.toString()
}
};
this.emit("nearby:request", { request });
this.logger.debug("Checking for nearby stores with request:", request);
const res = await this._loqate.storeFinder.findNearbyDistance(request);
this.logger.debug("Checking for nearby stores with response:", res);
this._patch({
destinationStores: res.destinationLocations,
nearby: {
count: this._store.get().nearby.count + 1,
response: res.destinationLocations,
requestHistory: [...this._store.get().nearby?.requestHistory ?? [], {
request,
options: {}
}]
}
});
this.emit("destinationStores:changed", res.destinationLocations ?? []);
this.emit("nearby:response", {
request,
response: res
});
return res;
} catch (error) {
this.logger.error("Nearby request failed:", error);
this.emit("error", error);
throw error;
}
}
async retrieve(request, options) {
try {
if (typeof request === "number") {
const items = this._store.get()?.typeahead?.find?.response ?? [];
if (request < 0 || request >= items.length) throw new Error(`Item index ${request} is out of bounds`);
request = items[request];
}
if (typeof request === "string") request = { addressID: request };
if (typeof request === "object" && "highlight" in request) {
if (!request.id) throw new Error(`Cannot retrieve item with no id`);
request = { id: request.id };
}
const finalRequest = {
input: "",
countries: "",
...this._store.get().typeahead?.retrieve?.request,
...request
};
this.logger.debug("Retrieving address with request:", finalRequest);
this.emit("typeahead:retrieve:request", { request: finalRequest });
let res = await this._loqate.storeFinder.geocodeGlobalTypeAhead(finalRequest, options);
this.logger.debug("Retrieving address with response:", res);
this._patch({
selectedLocation: {
latitude: res[0].location?.latitude,
longitude: res[0].location?.longitude
},
typeahead: {
count: this._store.get().typeahead?.count + 1,
find: { response: [] },
retrieve: {
count: this._store.get().typeahead?.retrieve?.count + 1,
addressID: finalRequest.addressID,
response: res?.[0],
requestHistory: [...this._store.get().typeahead?.retrieve?.requestHistory ?? [], {
request: finalRequest,
options
}]
}
}
});
this.emit("typeahead:retrieve:response", {
request: finalRequest,
response: res,
item: res?.[0]
});
this._locationChanged();
return res?.[0];
} catch (error) {
this.logger.error("Retrieve request failed:", error);
this.emit("error", error);
throw error;
}
}
async find(request, options) {
try {
const finalRequest = typeof request === "string" ? {
input: request,
countries: this._store.get().typeahead?.find?.request?.countries ?? ""
} : request;
if (finalRequest.addressID) throw new Error("AddressID cannot be used in Store Finder TypeAhead Find requests, call retrieve instead.");
this._patch({ typeahead: { find: { searchText: finalRequest.input } } });
request = {
...this._store.get().typeahead?.find?.request,
...this._generateBiasParams(),
...finalRequest
};
this.emit("typeahead:find:request", { request: finalRequest });
this.logger.debug("Finding with request:", finalRequest);
let response = await this._loqate.storeFinder.geocodeGlobalTypeAhead(request, options);
this.logger.debug("Finding with response:", response);
this._patch({ typeahead: {
count: this._store.get().typeahead?.count + 1,
find: {
count: this._store.get().typeahead?.find?.count + 1,
response,
requestHistory: [...this._store.get().typeahead?.find?.requestHistory ?? [], {
request: finalRequest,
options
}]
}
} });
this.emit("typeahead:find:response", {
request: finalRequest,
response
});
return response;
} catch (error) {
this.logger.error("Find request failed:", error);
this.emit("error", error);
throw error;
}
}
};
//#endregion
export { AddressSession, EmailSession, PhoneSession, StoreFinderSession };
//# sourceMappingURL=index.js.map