wineater-bot 4.22.0 → 4.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wineater-bot",
3
- "version": "4.22.0",
3
+ "version": "4.23.0",
4
4
  "description": "",
5
5
  "main": "dist/wineater-chatbot.mjs",
6
6
  "module": "src/index.js",
package/src/App.svelte CHANGED
@@ -31,7 +31,14 @@
31
31
  preconnectFontHosts();
32
32
 
33
33
  const INSTANT_CACHE_MAX_AGE_MS = 6 * 60 * 60 * 1000;
34
- const instantCacheKey = `${cacheScope}:instant`;
34
+ // A personal start grid follows the taste profile, which changes with every
35
+ // click — keep it for 1 h only; the general grid stays cached for 6 h.
36
+ const PERSONAL_INSTANT_CACHE_MAX_AGE_MS = 60 * 60 * 1000;
37
+ // The cache key carries the profile version from the last response
38
+ // (X-Wineater-Profile), remembered separately: a new profile → new key.
39
+ const instantProfileKey = `${cacheScope}:instant-profile`;
40
+ const instantCacheKey = (profileHash: string | null) =>
41
+ `${cacheScope}:instant${profileHash ? `:${profileHash}` : ''}`;
35
42
 
36
43
  const widgetType = window?.wineaterData?.WIDGET_TYPE || 'store';
37
44
  const countryCode = window?.wineaterData?.COUNTRY_CODE || null;
@@ -132,7 +139,11 @@
132
139
  // response only refreshes the cache for the next visit, so the grid the
133
140
  // visitor is already looking at never swaps under their cursor.
134
141
  async function loadInstantRecommendations(configReady: Promise<void>) {
135
- const cached = readCache<Wine[]>(instantCacheKey, INSTANT_CACHE_MAX_AGE_MS);
142
+ const lastProfile = readCache<string>(instantProfileKey, 30 * 24 * 60 * 60 * 1000);
143
+ const cached = readCache<Wine[]>(
144
+ instantCacheKey(lastProfile),
145
+ lastProfile ? PERSONAL_INSTANT_CACHE_MAX_AGE_MS : INSTANT_CACHE_MAX_AGE_MS
146
+ );
136
147
  const hasCached = !!cached?.length;
137
148
  if (hasCached) {
138
149
  results = cached;
@@ -140,8 +151,11 @@
140
151
  isLoading = true;
141
152
  }
142
153
  try {
143
- const fresh = await wineaterApi.getInstantRecommendations();
144
- if (fresh.length > 0) writeCache(instantCacheKey, fresh);
154
+ const { wines: fresh, profileHash } = await wineaterApi.getInstantRecommendations();
155
+ if (fresh.length > 0) {
156
+ writeCache(instantCacheKey(profileHash), fresh);
157
+ writeCache(instantProfileKey, profileHash);
158
+ }
145
159
  if (!hasCached && !userActed) results = fresh;
146
160
  } catch (error) {
147
161
  console.error('[wineater] instant recommendations error:', error);
@@ -68,6 +68,12 @@ class WineaterApi {
68
68
  // One place for every backend call: JSON body, auth, anonymous uid header,
69
69
  // uniform error. `keepalive` lets the last feedback batch out-live the page.
70
70
  async #request<T>(endpoint: string, body?: unknown, options: { keepalive?: boolean; method?: string } = {}): Promise<T> {
71
+ return (await this.#send<T>(endpoint, body, options)).data;
72
+ }
73
+
74
+ // Same as #request, but also hands back the response headers (instant/v2
75
+ // returns the taste-profile version in X-Wineater-Profile).
76
+ async #send<T>(endpoint: string, body?: unknown, options: { keepalive?: boolean; method?: string } = {}): Promise<{ data: T; headers: Headers }> {
71
77
  const init = (): RequestInit => ({
72
78
  method: options.method ?? 'POST',
73
79
  headers: this.#headers(),
@@ -91,16 +97,22 @@ class WineaterApi {
91
97
  if (!response.ok) {
92
98
  throw new Error(`API call failed with status ${response.status}: ${data?.message || 'No additional error information'}`);
93
99
  }
94
- return data as T;
100
+ return { data: data as T, headers: response.headers };
95
101
  }
96
102
 
97
- async getInstantRecommendations(): Promise<Wine[]> {
103
+ // `profileHash` is set when the backend served the visitor's personal start
104
+ // grid (anonymous taste profile); null for the general grid. The widget keys
105
+ // its instant cache by it, so a recomputed profile is picked up next visit.
106
+ async getInstantRecommendations(): Promise<{ wines: Wine[]; profileHash: string | null }> {
98
107
  try {
99
108
  const endpoint = this.#apiVersion === '2'
100
109
  ? 'dish/recommendations/instant/v2'
101
110
  : 'dish/recommendations/instant';
102
- const data = await this.#request<any[]>(endpoint, {});
103
- return data.filter(Boolean).map(deserializeInstantRecommendation);
111
+ const { data, headers } = await this.#send<any[]>(endpoint, {});
112
+ return {
113
+ wines: data.filter(Boolean).map(deserializeInstantRecommendation),
114
+ profileHash: headers.get('X-Wineater-Profile') || null,
115
+ };
104
116
  } catch (error) {
105
117
  throw new Error(`Error in getInstantRecommendations: ${error.message}`);
106
118
  }