@askdialog/dialog-sdk 2.11.0 → 2.13.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.
Files changed (48) hide show
  1. package/README.md +31 -20
  2. package/dist/Dialog.d.ts +16 -22
  3. package/dist/Dialog.d.ts.map +1 -1
  4. package/dist/Dialog.js +44 -56
  5. package/dist/__tests__/dialogChangeCartQuantity.spec.d.ts +2 -0
  6. package/dist/__tests__/dialogChangeCartQuantity.spec.d.ts.map +1 -0
  7. package/dist/__tests__/dialogChangeCartQuantity.spec.js +46 -0
  8. package/dist/__tests__/dialogSearchAnalytics.spec.js +2 -4
  9. package/dist/__tests__/publicTypes.spec.js +3 -0
  10. package/dist/__tests__/searchController.spec.js +46 -20
  11. package/dist/__tests__/searchControllerAttribution.spec.js +8 -6
  12. package/dist/__tests__/searchImpressions.spec.js +2 -3
  13. package/dist/__tests__/searchService.spec.js +13 -11
  14. package/dist/searchController.d.ts +1 -1
  15. package/dist/searchController.d.ts.map +1 -1
  16. package/dist/searchController.js +41 -64
  17. package/dist/services/search.d.ts +3 -5
  18. package/dist/services/search.d.ts.map +1 -1
  19. package/dist/services/search.js +11 -6
  20. package/dist/types/constructor.d.ts +9 -3
  21. package/dist/types/constructor.d.ts.map +1 -1
  22. package/dist/types/events.d.ts +5 -0
  23. package/dist/types/events.d.ts.map +1 -1
  24. package/dist/types/search.d.ts +14 -14
  25. package/dist/types/search.d.ts.map +1 -1
  26. package/dist/types/search.js +1 -1
  27. package/dist/types/searchAnalytics.d.ts +12 -6
  28. package/dist/types/searchAnalytics.d.ts.map +1 -1
  29. package/dist/types/searchAnalytics.js +1 -4
  30. package/dist/types/searchController.d.ts +22 -51
  31. package/dist/types/searchController.d.ts.map +1 -1
  32. package/dist/types/searchController.js +1 -1
  33. package/dist/utils/localization.d.ts +0 -1
  34. package/dist/utils/localization.d.ts.map +1 -1
  35. package/dist/utils/localization.js +0 -11
  36. package/dist/utils/searchControllerAnalytics.d.ts +1 -4
  37. package/dist/utils/searchControllerAnalytics.d.ts.map +1 -1
  38. package/dist/utils/searchControllerAnalytics.js +5 -8
  39. package/dist/utils/searchImpressions.d.ts +4 -4
  40. package/dist/utils/searchImpressions.d.ts.map +1 -1
  41. package/dist/utils/searchImpressions.js +4 -11
  42. package/dist/utils/searchRequests.d.ts +17 -0
  43. package/dist/utils/searchRequests.d.ts.map +1 -0
  44. package/dist/utils/searchRequests.js +36 -0
  45. package/package.json +1 -1
  46. package/dist/utils/searchSections.d.ts +0 -5
  47. package/dist/utils/searchSections.d.ts.map +0 -1
  48. package/dist/utils/searchSections.js +0 -21
@@ -1,4 +1,4 @@
1
- /* eslint max-lines: ["error", 460] */
1
+ /* eslint max-lines: ["error", 490] */
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { createSearchController } from "../searchController";
4
4
  import { SearchStatus } from "../types/searchController";
@@ -15,7 +15,7 @@ const DEBOUNCE_MS = 250;
15
15
  const response = (overrides = {}) => ({
16
16
  results: [
17
17
  {
18
- index: "products_fr",
18
+ index: "products_fr_eur",
19
19
  hits: [{ objectID: "p1" }, { objectID: "p2" }],
20
20
  nbHits: 2,
21
21
  page: 0,
@@ -37,7 +37,7 @@ const deferred = () => {
37
37
  });
38
38
  return { promise, resolve, reject };
39
39
  };
40
- // Lets the controller's `await search(...)` continuation run.
40
+ // Flush the search promise continuation.
41
41
  const settle = async () => {
42
42
  await Promise.resolve();
43
43
  await Promise.resolve();
@@ -49,12 +49,13 @@ const states = [];
49
49
  const createController = () => {
50
50
  const controller = createSearchController({
51
51
  search,
52
+ language: "fr",
53
+ currency: "EUR",
52
54
  analytics: {
53
55
  surface: "search_page",
54
56
  trackViewSearchResults,
55
57
  trackSelectSearchResult,
56
58
  },
57
- locale: "fr",
58
59
  });
59
60
  controller.subscribe((state) => states.push(state));
60
61
  return controller;
@@ -93,22 +94,28 @@ describe("createSearchController", () => {
93
94
  });
94
95
  expect(controller.getState().status).toBe(SearchStatus.SUCCESS);
95
96
  });
96
- it("sends one products entry named after the bare-language locale", async () => {
97
+ it("sends one products entry with independent language and currency", async () => {
97
98
  const controller = createSearchController({
98
99
  search,
100
+ language: "fr",
101
+ currency: "USD",
99
102
  analytics: {
100
103
  surface: "search_page",
101
104
  trackViewSearchResults,
102
105
  trackSelectSearchResult,
103
106
  },
104
- locale: "fr-FR",
105
107
  });
106
- search.mockResolvedValue(response({ query: "shoes" }));
108
+ search.mockResolvedValue(response({ query: "shoes", index: "products_fr_usd" }));
107
109
  controller.submit("shoes");
108
110
  await settle();
109
111
  expect(search.mock.calls[0][0]).toEqual({
110
112
  requests: [
111
- { indexName: "products_fr", query: "shoes", page: 0, hitsPerPage: 12 },
113
+ {
114
+ indexName: "products_fr_usd",
115
+ query: "shoes",
116
+ page: 0,
117
+ hitsPerPage: 12,
118
+ },
112
119
  ],
113
120
  });
114
121
  controller.dispose();
@@ -125,7 +132,7 @@ describe("createSearchController", () => {
125
132
  });
126
133
  expect(controller.getState().status).toBe(SearchStatus.SUCCESS);
127
134
  });
128
- it("aborts the previous in-flight request when a new one starts", async () => {
135
+ it("invalidates the previous request as soon as new input is queued", async () => {
129
136
  const controller = createController();
130
137
  const first = deferred();
131
138
  const second = deferred();
@@ -134,8 +141,14 @@ describe("createSearchController", () => {
134
141
  .mockReturnValueOnce(second.promise);
135
142
  controller.submit("shoes");
136
143
  const firstSignal = search.mock.calls[0][1]?.signal;
137
- controller.submit("boots");
144
+ controller.setQuery("boots");
145
+ first.resolve(response({ query: "shoes" }));
146
+ await settle();
138
147
  expect(firstSignal?.aborted).toBe(true);
148
+ expect(search).toHaveBeenCalledTimes(1);
149
+ expect(controller.getState().response).toBeUndefined();
150
+ expect(controller.getState().status).toBe(SearchStatus.LOADING);
151
+ vi.advanceTimersByTime(DEBOUNCE_MS);
139
152
  second.resolve(response({ query: "boots", queryID: "qid-2" }));
140
153
  await settle();
141
154
  expect(controller.getState().response?.query).toBe("boots");
@@ -151,7 +164,7 @@ describe("createSearchController", () => {
151
164
  controller.submit("boots");
152
165
  fresh.resolve(response({ query: "boots", queryID: "qid-2" }));
153
166
  await settle();
154
- // The stale transport ignored the abort and answers after the fresh one.
167
+ // Simulate a late response from a transport that ignores cancellation.
155
168
  stale.resolve(response({ query: "shoes", queryID: "qid-1" }));
156
169
  await settle();
157
170
  expect(controller.getState().status).toBe(SearchStatus.SUCCESS);
@@ -291,42 +304,54 @@ describe("createSearchController", () => {
291
304
  it("requests the sections on their first page with the products and exposes their entries", async () => {
292
305
  const controller = createSearchController({
293
306
  search,
307
+ language: "fr",
308
+ currency: "USD",
294
309
  analytics: {
295
310
  surface: "search_page",
296
311
  trackViewSearchResults,
297
312
  trackSelectSearchResult,
298
313
  },
299
- locale: "fr",
300
314
  sections: [{ index: "collections", hitsPerPage: 5 }],
301
315
  });
302
316
  const collections = {
303
317
  ...response().results[0],
304
- index: "collections_fr",
318
+ index: "collections_fr_usd",
305
319
  hits: [{ objectID: "c1" }],
306
320
  nbHits: 1,
307
321
  };
308
322
  search.mockResolvedValueOnce({
309
- results: [...response().results, collections],
323
+ results: [...response({ index: "products_fr_usd" }).results, collections],
310
324
  });
311
325
  controller.submit("shoes");
312
326
  await settle();
313
327
  expect(search.mock.calls[0][0].requests).toEqual([
314
- { indexName: "products_fr", query: "shoes", page: 0, hitsPerPage: 12 },
315
- { indexName: "collections_fr", query: "shoes", page: 0, hitsPerPage: 5 },
328
+ {
329
+ indexName: "products_fr_usd",
330
+ query: "shoes",
331
+ page: 0,
332
+ hitsPerPage: 12,
333
+ },
334
+ {
335
+ indexName: "collections_fr_usd",
336
+ query: "shoes",
337
+ page: 0,
338
+ hitsPerPage: 5,
339
+ },
316
340
  ]);
317
- expect(controller.getState().response?.index).toBe("products_fr");
341
+ expect(controller.getState().response?.index).toBe("products_fr_usd");
318
342
  expect(controller.getState().sections).toEqual({ collections });
319
343
  });
320
344
  it("resolves a sections function per request and leaves a missing entry out", async () => {
321
345
  let enabled = false;
322
346
  const controller = createSearchController({
323
347
  search,
348
+ language: "fr",
349
+ currency: "EUR",
324
350
  analytics: {
325
351
  surface: "search_page",
326
352
  trackViewSearchResults,
327
353
  trackSelectSearchResult,
328
354
  },
329
- locale: "fr",
330
355
  sections: () => (enabled ? [{ index: "collections" }] : []),
331
356
  });
332
357
  search.mockResolvedValue(response());
@@ -338,7 +363,7 @@ describe("createSearchController", () => {
338
363
  expect(search.mock.calls[0][0].requests).toHaveLength(1);
339
364
  expect(search.mock.calls[1][0].requests).toHaveLength(2);
340
365
  expect(search.mock.calls[1][0].requests[1]).toMatchObject({
341
- indexName: "collections_fr",
366
+ indexName: "collections_fr_eur",
342
367
  hitsPerPage: 12,
343
368
  });
344
369
  expect(controller.getState().sections).toEqual({});
@@ -347,12 +372,13 @@ describe("createSearchController", () => {
347
372
  const failure = new Error("entitlement lookup failed");
348
373
  const controller = createSearchController({
349
374
  search,
375
+ language: "fr",
376
+ currency: "EUR",
350
377
  analytics: {
351
378
  surface: "search_page",
352
379
  trackViewSearchResults,
353
380
  trackSelectSearchResult,
354
381
  },
355
- locale: "fr",
356
382
  sections: () => {
357
383
  throw failure;
358
384
  },
@@ -14,7 +14,7 @@ vi.mock("../utils/searchImpressions", () => ({
14
14
  const response = (overrides = {}) => ({
15
15
  results: [
16
16
  {
17
- index: "products_fr",
17
+ index: "products_fr_eur",
18
18
  hits: [
19
19
  { objectID: "p1", url: "https://shop.example/p1" },
20
20
  { objectID: "p2" },
@@ -36,13 +36,14 @@ const trackSelectSearchResult = vi.fn();
36
36
  const navigate = vi.fn();
37
37
  const createController = () => createSearchController({
38
38
  search,
39
+ language: "fr",
40
+ currency: "EUR",
39
41
  analytics: {
40
42
  surface: "search_page",
41
43
  trackViewSearchResults,
42
44
  trackSelectSearchResult,
43
45
  },
44
46
  navigate,
45
- locale: "fr",
46
47
  });
47
48
  beforeEach(() => {
48
49
  vi.useFakeTimers();
@@ -75,7 +76,7 @@ describe("search controller attribution", () => {
75
76
  await settle();
76
77
  expect(tracker.setContext).toHaveBeenCalledWith({
77
78
  query_id: "qid-1",
78
- index: "products_fr",
79
+ index: "products_fr_eur",
79
80
  surface: "search_page",
80
81
  search_type: "lexical",
81
82
  page: 2,
@@ -105,7 +106,7 @@ describe("search controller attribution", () => {
105
106
  await settle();
106
107
  expect(trackViewSearchResults).toHaveBeenCalledWith({
107
108
  query_id: "qid-1",
108
- index: "products_fr",
109
+ index: "products_fr_eur",
109
110
  surface: "search_page",
110
111
  search_type: "lexical",
111
112
  page: 1,
@@ -151,7 +152,7 @@ describe("search controller attribution", () => {
151
152
  });
152
153
  expect(trackSelectSearchResult).toHaveBeenCalledWith({
153
154
  query_id: "qid-1",
154
- index: "products_fr",
155
+ index: "products_fr_eur",
155
156
  surface: "search_page",
156
157
  search_type: "lexical",
157
158
  page: 1,
@@ -201,12 +202,13 @@ describe("search controller attribution", () => {
201
202
  it("returns false when no navigate adapter is configured", async () => {
202
203
  const controller = createSearchController({
203
204
  search,
205
+ language: "fr",
206
+ currency: "EUR",
204
207
  analytics: {
205
208
  surface: "search_page",
206
209
  trackViewSearchResults,
207
210
  trackSelectSearchResult,
208
211
  },
209
- locale: "fr",
210
212
  });
211
213
  search.mockResolvedValue(response());
212
214
  controller.submit("shoes");
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { createSearchImpressionTracker } from "../utils/searchImpressions";
4
4
  const envelope = {
5
5
  query_id: "query-1",
6
- index: "products_fr",
6
+ index: "products_fr_eur",
7
7
  surface: "search_page",
8
8
  search_type: "lexical",
9
9
  page: 1,
@@ -184,8 +184,7 @@ describe("createSearchImpressionTracker", () => {
184
184
  vi.advanceTimersByTime(300);
185
185
  windowListeners.get("pageshow")?.({ persisted: true });
186
186
  intersect(card, 0.6);
187
- // A stale pre-restore timer (200ms left) would record at 200ms and flush
188
- // at 1700ms; a fresh dwell records at 500ms and flushes at 2000ms.
187
+ // Fresh dwell: record at 500ms and flush at 2000ms, not at 200ms and 1700ms.
189
188
  vi.advanceTimersByTime(1700);
190
189
  expect(emit).not.toHaveBeenCalled();
191
190
  vi.advanceTimersByTime(300);
@@ -5,7 +5,7 @@ const API_KEY = "pk_test_abcdef";
5
5
  const request = {
6
6
  requests: [
7
7
  {
8
- indexName: "products_fr",
8
+ indexName: "products_fr_eur",
9
9
  query: "running shoes",
10
10
  page: 2,
11
11
  hitsPerPage: 50,
@@ -15,7 +15,7 @@ const request = {
15
15
  const emptyResponse = {
16
16
  results: [
17
17
  {
18
- index: "products_fr",
18
+ index: "products_fr_eur",
19
19
  hits: [],
20
20
  nbHits: 0,
21
21
  page: 0,
@@ -41,15 +41,17 @@ afterEach(() => {
41
41
  });
42
42
  describe("searchIndexName", () => {
43
43
  it.each([
44
- ["fr-FR", "products_fr"],
45
- ["pt-BR", "products_pt"],
46
- ["fr", "products_fr"],
47
- ["not a locale", "products_not a locale"],
48
- ])("reduces the locale %s to a bare-language index name", (locale, name) => {
49
- expect(searchIndexName("products", locale)).toBe(name);
44
+ ["fr", "EUR", "products_fr_eur"],
45
+ ["pt", "BRL", "products_pt_brl"],
46
+ ["fr", "CAD", "products_fr_cad"],
47
+ ["en", "EUR", "products_en_eur"],
48
+ ])("builds an index name from language %s and currency", (locale, currency, name) => {
49
+ expect(searchIndexName("products", locale, currency)).toBe(name);
50
50
  });
51
51
  it("names every logical index", () => {
52
- expect(searchIndexName("collections", "en-US")).toBe("collections_en");
52
+ expect(searchIndexName("collections", "en", "USD")).toBe("collections_en_usd");
53
+ expect(() => searchIndexName("products", "fr-FR", "EUR")).toThrow(/ISO 639-1/);
54
+ expect(() => searchIndexName("products", "fr", "")).toThrow(/ISO 4217/);
53
55
  });
54
56
  });
55
57
  describe("searchLexical", () => {
@@ -69,11 +71,11 @@ describe("searchLexical", () => {
69
71
  it("keeps optional per-entry fields the caller did not provide off the wire", async () => {
70
72
  fetchMock.mockResolvedValue(jsonResponse(emptyResponse));
71
73
  await searchLexical(API_KEY, {
72
- requests: [{ indexName: "products_fr", query: "running shoes" }],
74
+ requests: [{ indexName: "products_fr_eur", query: "running shoes" }],
73
75
  });
74
76
  const [, init] = fetchMock.mock.calls[0];
75
77
  expect(JSON.parse(init.body)).toEqual({
76
- requests: [{ indexName: "products_fr", query: "running shoes" }],
78
+ requests: [{ indexName: "products_fr_eur", query: "running shoes" }],
77
79
  });
78
80
  });
79
81
  it("resolves the typed response, including empty results", async () => {
@@ -1,3 +1,3 @@
1
1
  import { SearchController, SearchControllerOptions } from "./types/searchController";
2
- export declare function createSearchController({ search, analytics, navigate, debounceMs, hitsPerPage, locale, sections, }: SearchControllerOptions): SearchController;
2
+ export declare function createSearchController({ search, language, currency, analytics, navigate, debounceMs, hitsPerPage, sections, }: SearchControllerOptions): SearchController;
3
3
  //# sourceMappingURL=searchController.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"searchController.d.ts","sourceRoot":"","sources":["../src/searchController.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EAIxB,MAAM,0BAA0B,CAAC;AAqBlC,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,SAAS,EACT,QAAQ,EACR,UAAgC,EAChC,WAAmC,EACnC,MAAM,EACN,QAAa,GACd,EAAE,uBAAuB,GAAG,gBAAgB,CA6L5C"}
1
+ {"version":3,"file":"searchController.d.ts","sourceRoot":"","sources":["../src/searchController.ts"],"names":[],"mappings":"AACA,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EAGxB,MAAM,0BAA0B,CAAC;AAiBlC,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,UAAgB,EAChB,WAAgB,EAChB,QAAa,GACd,EAAE,uBAAuB,GAAG,gBAAgB,CAuK5C"}
@@ -1,16 +1,7 @@
1
- // This controller is one cohesive unit: `selectResult` reports whether the
2
- // navigate adapter handled the transition and the response is split into the
3
- // products entry plus its sections, pushing the file past the default 200-line
4
- // cap — raised modestly rather than split artificially.
5
- /* eslint max-lines: ["error", 240] */
6
1
  import { searchIndexName } from "./services/search";
7
2
  import { SearchStatus, } from "./types/searchController";
8
3
  import { createControllerAnalytics } from "./utils/searchControllerAnalytics";
9
- import { buildSectionQueries, pickSectionResults, } from "./utils/searchSections";
10
- const DEFAULT_DEBOUNCE_MS = 250;
11
- const DEFAULT_HITS_PER_PAGE = 12;
12
- // `SearchRequest` rejects queries under two visible characters (code points).
13
- const MIN_QUERY_CODE_POINTS = 2;
4
+ import { buildSearchRequest, normalizeQuery, readSearchResults, } from "./utils/searchRequests";
14
5
  const INITIAL_STATE = {
15
6
  status: SearchStatus.IDLE,
16
7
  query: "",
@@ -19,18 +10,19 @@ const INITIAL_STATE = {
19
10
  sections: undefined,
20
11
  error: undefined,
21
12
  };
22
- export function createSearchController({ search, analytics, navigate, debounceMs = DEFAULT_DEBOUNCE_MS, hitsPerPage = DEFAULT_HITS_PER_PAGE, locale, sections = [], }) {
23
- const indexName = searchIndexName("products", locale);
24
- const resolveSections = () => typeof sections === "function" ? sections() : sections;
13
+ export function createSearchController({ search, language, currency, analytics, navigate, debounceMs = 250, hitsPerPage = 12, sections = [], }) {
14
+ const requestConfig = {
15
+ indexName: searchIndexName("products", language, currency),
16
+ language,
17
+ currency,
18
+ hitsPerPage,
19
+ };
25
20
  let state = INITIAL_STATE;
26
21
  const listeners = new Set();
27
22
  const controllerAnalytics = createControllerAnalytics(analytics);
28
23
  let debounceTimer;
29
- // Query typed but not yet run: `state.query` only holds committed queries, so
30
- // without this, `setPage()` would cancel the debounce and paginate stale results.
31
24
  let pendingQuery;
32
25
  let abortController;
33
- // Monotonic token: a response only lands if it belongs to the latest request.
34
26
  let requestId = 0;
35
27
  let disposed = false;
36
28
  const setState = (patch) => {
@@ -39,45 +31,40 @@ export function createSearchController({ search, analytics, navigate, debounceMs
39
31
  listener(state);
40
32
  }
41
33
  };
42
- const cancelInFlight = () => {
34
+ const cancelPendingSearch = () => {
43
35
  clearTimeout(debounceTimer);
36
+ pendingQuery = undefined;
37
+ requestId += 1;
44
38
  abortController?.abort();
45
39
  abortController = undefined;
46
40
  };
47
- const buildRequest = (query, page, requested) => ({
48
- requests: [
49
- { indexName, query, page, hitsPerPage },
50
- ...buildSectionQueries(requested, query, locale, hitsPerPage),
51
- ],
52
- });
53
41
  const run = async (query, page) => {
54
- cancelInFlight();
42
+ cancelPendingSearch();
55
43
  abortController = new AbortController();
56
- const id = ++requestId;
44
+ const id = requestId;
57
45
  setState({ status: SearchStatus.LOADING, query, page });
58
46
  try {
59
- const requested = resolveSections();
60
- const response = await search(buildRequest(query, page, requested), {
47
+ const requested = typeof sections === "function" ? sections() : sections;
48
+ const request = buildSearchRequest(query, page, requested, requestConfig);
49
+ const response = await search(request, {
61
50
  signal: abortController.signal,
62
51
  });
63
52
  if (id !== requestId) {
64
- return; // A newer request landed first: this response is stale.
65
- }
66
- const result = response.results.find((entry) => entry.index === indexName);
67
- if (result === undefined) {
68
- throw new Error(`Dialog search returned no ${indexName} entry`);
53
+ return; // Ignore a superseded request.
69
54
  }
70
- controllerAnalytics.onResponse(result);
55
+ const results = readSearchResults(response, requested, requestConfig);
56
+ controllerAnalytics.onResponse(results.response);
71
57
  setState({
72
- status: result.nbHits === 0 ? SearchStatus.EMPTY : SearchStatus.SUCCESS,
73
- response: result,
74
- sections: pickSectionResults(response, requested, locale),
58
+ ...results,
59
+ status: results.response.nbHits === 0
60
+ ? SearchStatus.EMPTY
61
+ : SearchStatus.SUCCESS,
75
62
  error: undefined,
76
63
  });
77
64
  }
78
65
  catch (error) {
79
66
  if (error instanceof DOMException && error.name === "AbortError") {
80
- return; // Superseded by a newer request — never surfaced.
67
+ return; // Cancellation is not a search error.
81
68
  }
82
69
  if (id !== requestId) {
83
70
  return;
@@ -90,33 +77,28 @@ export function createSearchController({ search, analytics, navigate, debounceMs
90
77
  });
91
78
  }
92
79
  };
93
- // Shared by `setQuery`/`submit`: undefined = reset to idle (query too short).
94
- const prepare = (rawQuery) => {
95
- const query = rawQuery.trim();
96
- cancelInFlight();
97
- pendingQuery = undefined;
98
- if ([...query].length < MIN_QUERY_CODE_POINTS) {
99
- requestId += 1; // Invalidate anything in flight.
100
- setState(INITIAL_STATE);
101
- return undefined;
102
- }
103
- return query;
104
- };
105
80
  return {
106
81
  setQuery(rawQuery) {
107
- const query = disposed ? undefined : prepare(rawQuery);
82
+ if (disposed) {
83
+ return;
84
+ }
85
+ cancelPendingSearch();
86
+ const query = normalizeQuery(rawQuery);
108
87
  if (query === undefined) {
88
+ setState(INITIAL_STATE);
109
89
  return;
110
90
  }
111
91
  pendingQuery = query;
112
- debounceTimer = setTimeout(() => {
113
- pendingQuery = undefined;
114
- void run(query, 0);
115
- }, debounceMs);
92
+ debounceTimer = setTimeout(() => void run(query, 0), debounceMs);
116
93
  },
117
94
  submit(rawQuery) {
118
- const query = disposed ? undefined : prepare(rawQuery);
95
+ if (disposed) {
96
+ return;
97
+ }
98
+ cancelPendingSearch();
99
+ const query = normalizeQuery(rawQuery);
119
100
  if (query === undefined) {
101
+ setState(INITIAL_STATE);
120
102
  return;
121
103
  }
122
104
  void run(query, 0);
@@ -126,15 +108,12 @@ export function createSearchController({ search, analytics, navigate, debounceMs
126
108
  return;
127
109
  }
128
110
  if (pendingQuery !== undefined) {
129
- // A query change is pending: the click targeted stale results, so the
130
- // new query supersedes pagination — flush it now instead of debouncing.
131
- const query = pendingQuery;
132
- pendingQuery = undefined;
133
- void run(query, 0);
111
+ // Submit pending input on page zero before allowing pagination.
112
+ void run(pendingQuery, 0);
134
113
  return;
135
114
  }
136
115
  if (state.query === "") {
137
- return; // Pagination requires a committed non-empty query.
116
+ return; // No query to paginate.
138
117
  }
139
118
  void run(state.query, page);
140
119
  },
@@ -173,9 +152,7 @@ export function createSearchController({ search, analytics, navigate, debounceMs
173
152
  getState: () => state,
174
153
  dispose() {
175
154
  disposed = true;
176
- cancelInFlight();
177
- requestId += 1;
178
- pendingQuery = undefined;
155
+ cancelPendingSearch();
179
156
  listeners.clear();
180
157
  controllerAnalytics.dispose();
181
158
  },
@@ -1,8 +1,6 @@
1
1
  import { SearchIndex, SearchOptions, SearchRequest, SearchResponse } from "../types/search";
2
- export declare const searchIndexName: (index: SearchIndex, locale: string) => string;
3
- /**
4
- * One POST per invocation no debounce, cache, retry or request state; the
5
- * caller owns cancellation through `options.signal`.
6
- */
2
+ /** Build an index name from an ISO 639-1 language and ISO 4217 currency. */
3
+ export declare const searchIndexName: (index: SearchIndex, language: string, currency: string) => string;
4
+ /** Send one request. The caller controls cancellation through `options.signal`. */
7
5
  export declare const searchLexical: (apiKey: string, request: SearchRequest, options?: SearchOptions) => Promise<SearchResponse>;
8
6
  //# sourceMappingURL=search.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/services/search.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,WAAW,EACX,aAAa,EACb,aAAa,EACb,cAAc,EACf,MAAM,iBAAiB,CAAC;AAMzB,eAAO,MAAM,eAAe,GAAI,OAAO,WAAW,EAAE,QAAQ,MAAM,KAAG,MACzB,CAAC;AAoB7C;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,QAAQ,MAAM,EACd,SAAS,aAAa,EACtB,UAAU,aAAa,KACtB,OAAO,CAAC,cAAc,CAgBxB,CAAC"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/services/search.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,WAAW,EACX,aAAa,EACb,aAAa,EACb,cAAc,EACf,MAAM,iBAAiB,CAAC;AAKzB,4EAA4E;AAC5E,eAAO,MAAM,eAAe,GAC1B,OAAO,WAAW,EAClB,UAAU,MAAM,EAChB,UAAU,MAAM,KACf,MASF,CAAC;AAoBF,mFAAmF;AACnF,eAAO,MAAM,aAAa,GACxB,QAAQ,MAAM,EACd,SAAS,aAAa,EACtB,UAAU,aAAa,KACtB,OAAO,CAAC,cAAc,CAgBxB,CAAC"}
@@ -1,9 +1,17 @@
1
1
  import { config } from "../config";
2
2
  import { DialogSearchError } from "../DialogSearchError";
3
- import { toIso639LanguageCode } from "../utils/localization";
4
3
  const SEARCH_PATH = "/public/search/lexical";
5
4
  const API_KEY_HEADER = "x-dialog-api-key";
6
- export const searchIndexName = (index, locale) => `${index}_${toIso639LanguageCode(locale)}`;
5
+ /** Build an index name from an ISO 639-1 language and ISO 4217 currency. */
6
+ export const searchIndexName = (index, language, currency) => {
7
+ if (!/^[a-z]{2}$/.test(language)) {
8
+ throw new Error("Search language must be a lowercase ISO 639-1 code.");
9
+ }
10
+ if (!/^[A-Za-z]{3}$/.test(currency)) {
11
+ throw new Error("Search currency must be an ISO 4217 code.");
12
+ }
13
+ return `${index}_${language}_${currency.toLowerCase()}`;
14
+ };
7
15
  const toSearchError = async (response) => {
8
16
  let body;
9
17
  try {
@@ -18,10 +26,7 @@ const toSearchError = async (response) => {
18
26
  : response.statusText;
19
27
  return new DialogSearchError({ status: response.status, code, message });
20
28
  };
21
- /**
22
- * One POST per invocation — no debounce, cache, retry or request state; the
23
- * caller owns cancellation through `options.signal`.
24
- */
29
+ /** Send one request. The caller controls cancellation through `options.signal`. */
25
30
  export const searchLexical = async (apiKey, request, options) => {
26
31
  const response = await fetch(`${config.monolithApiUrl}${SEARCH_PATH}`, {
27
32
  method: "POST",
@@ -1,9 +1,10 @@
1
- import { AddToCartInput } from "./events";
1
+ import { AddToCartInput, ChangeCartQuantityInput } from "./events";
2
2
  import { SimplifiedProduct } from "./product";
3
3
  import { Theme } from "./theme";
4
4
  export interface DialogCallbacks {
5
5
  addToCart: (input: AddToCartInput) => Promise<void>;
6
6
  getProduct: (productId: string, variantId?: string) => Promise<SimplifiedProduct>;
7
+ changeCartQuantity?: (input: ChangeCartQuantityInput) => Promise<void>;
7
8
  }
8
9
  export interface CurrentProduct {
9
10
  /** The catalog product id — must match the id in the Dialog product feed. */
@@ -12,7 +13,10 @@ export interface CurrentProduct {
12
13
  }
13
14
  export interface DialogConstructor {
14
15
  apiKey: string;
16
+ /** BCP-47 locale, e.g. `fr-FR`. */
15
17
  locale: string;
18
+ /** Currency (ISO 4217), independent of language. */
19
+ currency: string;
16
20
  /**
17
21
  * Optional ISO 3166 country code used to format prices (e.g. 'FR', 'US').
18
22
  * When provided it takes precedence over the country derived from `locale`.
@@ -21,8 +25,10 @@ export interface DialogConstructor {
21
25
  countryCode?: string;
22
26
  /**
23
27
  * Commerce callbacks. Optional: a search-only integration can omit them —
24
- * the assistant runtime still loads, and only `getProduct()` / `addToCart()`
25
- * throw a configuration error when their callback is absent.
28
+ * the assistant runtime still loads, and `getProduct()`, `addToCart()` and
29
+ * `changeCartQuantity()` throw a configuration error when their callback is
30
+ * absent. `changeCartQuantity` is optional within the set: check
31
+ * `canChangeCartQuantity()` before calling it.
26
32
  */
27
33
  callbacks?: DialogCallbacks;
28
34
  theme?: Partial<Theme>;
@@ -1 +1 @@
1
- {"version":3,"file":"constructor.d.ts","sourceRoot":"","sources":["../../src/types/constructor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,UAAU,EAAE,CACV,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,KACf,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B"}
1
+ {"version":3,"file":"constructor.d.ts","sourceRoot":"","sources":["../../src/types/constructor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,UAAU,EAAE,CACV,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,KACf,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAChC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxE;AAED,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B"}
@@ -51,6 +51,11 @@ export interface AddToCartInput extends AddToCartProductDetails {
51
51
  currency?: string;
52
52
  variantId?: string;
53
53
  }
54
+ export interface ChangeCartQuantityInput {
55
+ productId: string;
56
+ variantId?: string;
57
+ quantity: number;
58
+ }
54
59
  export interface TrackEventPayload extends AddToCartProductDetails {
55
60
  userId?: string;
56
61
  productId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/types/events.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,EAC9B,6BAA6B,EAC9B,MAAM,mBAAmB,CAAC;AAE3B,eAAO,MAAM,mBAAmB,+BAA+B,CAAC;AAEhE,oBAAY,YAAY;IACtB,cAAc,mBAAmB;IACjC,eAAe,oBAAoB;IACnC,YAAY,qBAAqB;IACjC,qBAAqB,qBAAqB;IAC1C,iBAAiB,sBAAsB;IACvC,qBAAqB,0BAA0B;IAC/C,yBAAyB,8BAA8B;IACvD,0BAA0B,+BAA+B;CAC1D;AACD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,sBAAuB,SAAQ,sBAAsB;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,GAAG,cAAc,CAAC;AAExE,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,oBAAoB,CAAC;IACjC,GAAG,EAAE,MAAM,CAAC;CACb;AAQD,MAAM,WAAW,uBAAuB;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAKD,MAAM,WAAW,cAAe,SAAQ,uBAAuB;IAC7D,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAQD,MAAM,WAAW,iBAAkB,SAAQ,uBAAuB;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAKD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,MAAM,WAAW,0BAA0B;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC5B;AAGD,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,kBAAkB,GAC1B,sBAAsB,GACtB,sBAAsB,GACtB,iBAAiB,GACjB,oBAAoB,GACpB,iBAAiB,GACjB,0BAA0B,GAC1B,6BAA6B,GAC7B,8BAA8B,CAAC;AAEnC,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,kBAAkB;IACjD,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,EAAE,CAAC,CAAC;CACZ"}
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/types/events.ts"],"names":[],"mappings":"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"}