@gomusdev/web-components 4.17.1 → 4.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.18.0 (2026-08-18)
4
+
5
+ ### Features
6
+
7
+ * **go:** expose customer write endpoints on go.api
8
+ * **shop:** cache invalidation by endpoint prefix
9
+ * **shop:** createCustomerAddress
10
+ * **shop:** update/delete customer address
11
+ * **shop:** updateCustomer profile update
12
+ * **types:** schema types for customer write endpoints
13
+
14
+ ### Bug Fixes
15
+
16
+ * **profile:** localize the not-signed-in error in go-profile-password
17
+ * **shop:** harden the customer writes after review
18
+ * **shop:** only street, zip and city are required on address create
19
+ * **shop:** stop asyncFetch spinning forever on uncached reads
20
+ * **types,docs:** match the write error types and success gates to real openapi-fetch behavior
21
+ * **types:** regenerate schema from the corrected v4 swagger
22
+
3
23
  ## 4.17.1 (2026-08-14)
4
24
 
5
25
  ### Bug Fixes
@@ -6385,7 +6385,8 @@ createHTML: (html) => {
6385
6385
  "cart.item.remove": "✕",
6386
6386
  "common.table.donation": "Donation",
6387
6387
  "cart.donation.title": "Donation",
6388
- "donations.checkbox.label": "Add a {{amount}} donation to {{campaign}}"
6388
+ "donations.checkbox.label": "Add a {{amount}} donation to {{campaign}}",
6389
+ "Not signed in": "Not signed in"
6389
6390
  },
6390
6391
  de: {
6391
6392
  "quantity.remove": "Artikel entfernen",
@@ -6393,7 +6394,8 @@ createHTML: (html) => {
6393
6394
  "cart.item.remove": "✕",
6394
6395
  "common.table.donation": "Spende",
6395
6396
  "cart.donation.title": "Spende",
6396
- "donations.checkbox.label": "{{amount}} für {{campaign}} spenden"
6397
+ "donations.checkbox.label": "{{amount}} für {{campaign}} spenden",
6398
+ "Not signed in": "Nicht angemeldet"
6397
6399
  }
6398
6400
  };
6399
6401
  //#endregion
@@ -13720,12 +13722,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
13720
13722
  var TICKET_AND_QUOTAS_ENDPOINT = "/api/v4/tickets/list_and_quotas";
13721
13723
  var TICKETS_ENDPOINT = "/api/v4/tickets";
13722
13724
  var SIGN_IN_ENDPOINT = "/api/v4/auth/sign_in";
13723
- var SIGN_UP_ENDPOINT = "/api/v4/auth";
13725
+ var AUTH_ENDPOINT = "/api/v4/auth";
13724
13726
  var WITHDRAWAL_ENDPOINT = "/api/v4/orders/withdrawals";
13725
13727
  var MEMBERSHIP_ACTIVATION_ENDPOINT = "/api/v4/customer/memberships/activate";
13726
13728
  var ORDERS_ENDPOINT = "/api/v4/orders";
13727
13729
  var CUSTOMER_ADDRESSES_ENDPOINT = "/api/v4/customer/customer_addresses";
13728
13730
  var CUSTOMER_MEMBERSHIPS_ENDPOINT = "/api/v4/customer/memberships";
13731
+ var CUSTOMER_ADDRESS_ENDPOINT = "/api/v4/customer/customer_addresses/{id}";
13729
13732
  //#endregion
13730
13733
  //#region ../../packages/gomus-api/lib/customerLevels.ts
13731
13734
  var CustomerLevels = {
@@ -13747,6 +13750,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
13747
13750
  locales: []
13748
13751
  }));
13749
13752
  #fetchStatus = {};
13753
+ #invalidationGens = {};
13754
+ #collectReads = null;
13750
13755
  constructor(apiUrl, shopDomain, locale, type = "angular") {
13751
13756
  this.type = type;
13752
13757
  if (apiUrl && shopDomain && locale) this.load(apiUrl, shopDomain, locale, type);
@@ -13866,6 +13871,66 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
13866
13871
  if (!this.auth.data.accessToken) return NOT_SIGNED_IN;
13867
13872
  return this.fetchAndCache(CUSTOMER_MEMBERSHIPS_ENDPOINT, "customerMemberships", "", { cache: 5 });
13868
13873
  }
13874
+ /**
13875
+ * Shared skeleton of every customer write: the no-local-token guard (a write
13876
+ * must never ride on a same-origin session cookie), the request itself, and
13877
+ * the invalidate-on-ok tail that makes the matching cached read refetch.
13878
+ * Pass `null` when no cached read depends on the written resource.
13879
+ */
13880
+ async #authedWrite(invalidates, call) {
13881
+ if (!this.auth.data.accessToken) return NOT_SIGNED_IN;
13882
+ const result = await call();
13883
+ if (invalidates && result?.response?.ok) this.invalidate(invalidates);
13884
+ return result;
13885
+ }
13886
+ /**
13887
+ * Creates a saved address for the signed-in customer, then drops the cached
13888
+ * address list so the next getCustomerAddresses() refetches.
13889
+ * The backend body is root-keyed ({ customer_address: {...} }) — rootKey makes
13890
+ * apiCall validate required fields on the inner params, because the backend's
13891
+ * 422 carries no body and would give integrators nothing actionable.
13892
+ */
13893
+ async createCustomerAddress(params) {
13894
+ return this.#authedWrite(CUSTOMER_ADDRESSES_ENDPOINT, () => this.apiPost(CUSTOMER_ADDRESSES_ENDPOINT, {
13895
+ body: { customer_address: params },
13896
+ requiredFields: [
13897
+ "street",
13898
+ "zip",
13899
+ "city"
13900
+ ],
13901
+ rootKey: "customer_address"
13902
+ }));
13903
+ }
13904
+ /**
13905
+ * Updates a saved address (partial body — the backend applies only the sent
13906
+ * fields), then drops the cached address list. No client-side requiredFields:
13907
+ * every field is optional on update.
13908
+ */
13909
+ async updateCustomerAddress(id, params) {
13910
+ return this.#authedWrite(CUSTOMER_ADDRESSES_ENDPOINT, () => this.apiPut(CUSTOMER_ADDRESS_ENDPOINT, {
13911
+ body: { customer_address: params },
13912
+ params: { path: { id } }
13913
+ }));
13914
+ }
13915
+ /**
13916
+ * Deletes a saved address, then drops the cached address list. Success is
13917
+ * 204 No Content — parseAs 'text' skips openapi-fetch's JSON parse (see apiCall).
13918
+ */
13919
+ async deleteCustomerAddress(id) {
13920
+ return this.#authedWrite(CUSTOMER_ADDRESSES_ENDPOINT, () => this.apiDELETE(CUSTOMER_ADDRESS_ENDPOINT, {
13921
+ params: { path: { id } },
13922
+ parseAs: "text"
13923
+ }));
13924
+ }
13925
+ /**
13926
+ * Updates the signed-in customer's profile (PUT /api/v4/auth — devise
13927
+ * registrations#update). Profile fields only: password changes go through
13928
+ * updatePassword, which enforces current_password. Invalidates the cached
13929
+ * validate_token payload so getCustomer() refetches the fresh profile.
13930
+ */
13931
+ async updateCustomer(params) {
13932
+ return this.#authedWrite(VALIDATE_TOKEN_ENDPOINT, () => this.apiPut(AUTH_ENDPOINT, { body: params }));
13933
+ }
13869
13934
  ticketsCalendar(params) {
13870
13935
  return this.fetchAndCache(TICKETS_CALENDAR_ENDPOINT, `ticketsCalendar-${JSON.stringify(params)}`, "data", {
13871
13936
  cache: 60,
@@ -13918,7 +13983,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
13918
13983
  "terms"
13919
13984
  ];
13920
13985
  if (!asGuest) requiredFields.push("password", "password_confirmation");
13921
- return this.apiPost(SIGN_UP_ENDPOINT, {
13986
+ return this.apiPost(AUTH_ENDPOINT, {
13922
13987
  body: params,
13923
13988
  requiredFields
13924
13989
  });
@@ -13946,15 +14011,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
13946
14011
  cache: 60
13947
14012
  });
13948
14013
  }
13949
- updatePassword(params) {
13950
- return this.apiPut("/api/v4/auth/password", {
14014
+ async updatePassword(params) {
14015
+ return this.#authedWrite(null, () => this.apiPut("/api/v4/auth/password", {
13951
14016
  body: params,
13952
14017
  requiredFields: [
13953
14018
  "current_password",
13954
14019
  "password",
13955
14020
  "password_confirmation"
13956
14021
  ]
13957
- });
14022
+ }));
13958
14023
  }
13959
14024
  passwordReset(params) {
13960
14025
  return this.apiPost("/api/v4/auth/password", {
@@ -14029,6 +14094,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14029
14094
  }
14030
14095
  });
14031
14096
  }
14097
+ /**
14098
+ * Drops every cached GET slot whose fetch id starts with the endpoint prefix,
14099
+ * so the next fetchAndCache read refetches. #data is left intact — consumers
14100
+ * keep rendering the last known value until the refetch lands (no flash of
14101
+ * empty). Write methods call this after mutating the corresponding resource.
14102
+ *
14103
+ * In-flight slots are marked instead of deleted: their response predates the
14104
+ * write, but deleting them would release waitForAllFetches early AND let
14105
+ * apiGet resurrect the slot as fresh when the stale response lands. apiGet
14106
+ * drops marked slots on completion, so the next read refetches.
14107
+ */
14108
+ invalidate(endpointPrefix) {
14109
+ for (const [fetchId, slot] of Object.entries(this.#fetchStatus)) {
14110
+ if (!fetchId.startsWith(endpointPrefix)) continue;
14111
+ if (slot.status === "fetching") slot.invalidated = true;
14112
+ else delete this.#fetchStatus[fetchId];
14113
+ this.#invalidationGens[fetchId] = (this.#invalidationGens[fetchId] ?? 0) + 1;
14114
+ }
14115
+ }
14032
14116
  #fetchId(endpoint, query, path = {}) {
14033
14117
  return endpoint + JSON.stringify(query) + JSON.stringify(path);
14034
14118
  }
@@ -14054,6 +14138,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14054
14138
  }
14055
14139
  const query = options.query;
14056
14140
  const fetchId = this.#fetchId(endpoint, query, options.path);
14141
+ this.#collectReads?.(fetchId);
14057
14142
  const isNotFetchedYet = !this.#fetchStatus[fetchId];
14058
14143
  const isCacheExpired = this.#fetchStatus[fetchId]?.fetchedAt < Date.now() - options.cache * 1e3;
14059
14144
  if (isNotFetchedYet || isCacheExpired) this.apiGet(endpoint, query, options.path).then((ret) => {
@@ -14164,8 +14249,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14164
14249
  }
14165
14250
  async apiCall(path, options) {
14166
14251
  this.#ensureApi();
14167
- const { body, params = {}, requiredFields, parseAs } = options;
14168
- const validationErrors = validateApiPostBody(body, requiredFields);
14252
+ const { body, params = {}, requiredFields, rootKey, parseAs } = options;
14253
+ const validationErrors = validateApiPostBody((rootKey ? body?.[rootKey] : body) ?? {}, requiredFields);
14169
14254
  if (Object.keys(validationErrors).length > 0) return { error: { errors: validationErrors } };
14170
14255
  const httpMethod = this.client[options.method];
14171
14256
  return await httpMethod(path, {
@@ -14179,8 +14264,28 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14179
14264
  * and returns the result of the method execution.
14180
14265
  */
14181
14266
  async asyncFetch(method) {
14182
- await this.waitForAllFetches(method());
14183
- return method();
14267
+ const seenGens = /* @__PURE__ */ new Map();
14268
+ const call = () => {
14269
+ const prev = this.#collectReads;
14270
+ this.#collectReads = (id) => {
14271
+ if (!seenGens.has(id)) seenGens.set(id, this.#invalidationGens[id] ?? 0);
14272
+ };
14273
+ try {
14274
+ return method();
14275
+ } finally {
14276
+ this.#collectReads = prev;
14277
+ }
14278
+ };
14279
+ const invalidatedSinceRead = () => [...seenGens].some(([id, gen]) => (this.#invalidationGens[id] ?? 0) !== gen);
14280
+ call();
14281
+ await this.waitForAllFetches();
14282
+ let result = call();
14283
+ while (invalidatedSinceRead()) {
14284
+ for (const id of seenGens.keys()) seenGens.set(id, this.#invalidationGens[id] ?? 0);
14285
+ await this.waitForAllFetches();
14286
+ result = call();
14287
+ }
14288
+ return result;
14184
14289
  }
14185
14290
  async waitForAllFetches(...variables) {
14186
14291
  while (Object.values(this.#fetchStatus).filter((f) => f.status === "fetching").length) await wait(10);
@@ -14196,7 +14301,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14196
14301
  if (query) params = assign(params, { params: { query } });
14197
14302
  if (path) params = assign(params, { params: { path: pathOptions } });
14198
14303
  const ret = await this.client.GET(path, params);
14199
- this.#fetchStatus[fetchId] = {
14304
+ if (this.#fetchStatus[fetchId]?.invalidated) delete this.#fetchStatus[fetchId];
14305
+ else this.#fetchStatus[fetchId] = {
14200
14306
  status: "completed",
14201
14307
  fetchedAt: Date.now()
14202
14308
  };
@@ -37565,10 +37671,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
37565
37671
  async function submit(event) {
37566
37672
  const details = event.target.details;
37567
37673
  const result = await shop.updatePassword(details.formData);
37568
- if (result.data) {
37674
+ if ("data" in result && result.data) {
37569
37675
  details.successMessage = shop.t("user.passwordSuccess.desc.title");
37570
37676
  details.apiErrors = [];
37571
- } else details.apiErrors = result.error?.errors || result.error || result.errors;
37677
+ } else {
37678
+ const errors = ("error" in result ? result.error : void 0)?.errors ?? [];
37679
+ details.apiErrors = Array.isArray(errors) ? errors.map((e) => shop.t(e)) : errors;
37680
+ }
37572
37681
  }
37573
37682
  init();
37574
37683
  var go_form = root$6();
@@ -39148,6 +39257,30 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
39148
39257
  getCustomerMemberships: async () => {
39149
39258
  await ensureShopReady();
39150
39259
  return shop.asyncFetch(() => shop.getCustomerMemberships());
39260
+ },
39261
+ createCustomerAddress: async (params) => {
39262
+ await ensureShopReady();
39263
+ return shop.createCustomerAddress(params);
39264
+ },
39265
+ updateCustomerAddress: async (id, params) => {
39266
+ await ensureShopReady();
39267
+ return shop.updateCustomerAddress(id, params);
39268
+ },
39269
+ deleteCustomerAddress: async (id) => {
39270
+ await ensureShopReady();
39271
+ return shop.deleteCustomerAddress(id);
39272
+ },
39273
+ updateCustomer: async (params) => {
39274
+ await ensureShopReady();
39275
+ return shop.updateCustomer(params);
39276
+ },
39277
+ updatePassword: async (params) => {
39278
+ await ensureShopReady();
39279
+ return shop.updatePassword(params);
39280
+ },
39281
+ requestPasswordReset: async (params) => {
39282
+ await ensureShopReady();
39283
+ return shop.passwordReset(params);
39151
39284
  }
39152
39285
  },
39153
39286
  cart: { addItem: async (options) => {
@@ -6384,7 +6384,8 @@ var defaultTranslations_default = {
6384
6384
  "cart.item.remove": "✕",
6385
6385
  "common.table.donation": "Donation",
6386
6386
  "cart.donation.title": "Donation",
6387
- "donations.checkbox.label": "Add a {{amount}} donation to {{campaign}}"
6387
+ "donations.checkbox.label": "Add a {{amount}} donation to {{campaign}}",
6388
+ "Not signed in": "Not signed in"
6388
6389
  },
6389
6390
  de: {
6390
6391
  "quantity.remove": "Artikel entfernen",
@@ -6392,7 +6393,8 @@ var defaultTranslations_default = {
6392
6393
  "cart.item.remove": "✕",
6393
6394
  "common.table.donation": "Spende",
6394
6395
  "cart.donation.title": "Spende",
6395
- "donations.checkbox.label": "{{amount}} für {{campaign}} spenden"
6396
+ "donations.checkbox.label": "{{amount}} für {{campaign}} spenden",
6397
+ "Not signed in": "Nicht angemeldet"
6396
6398
  }
6397
6399
  };
6398
6400
  //#endregion
@@ -13719,12 +13721,13 @@ var VALIDATE_TOKEN_ENDPOINT = "/api/v4/auth/validate_token";
13719
13721
  var TICKET_AND_QUOTAS_ENDPOINT = "/api/v4/tickets/list_and_quotas";
13720
13722
  var TICKETS_ENDPOINT = "/api/v4/tickets";
13721
13723
  var SIGN_IN_ENDPOINT = "/api/v4/auth/sign_in";
13722
- var SIGN_UP_ENDPOINT = "/api/v4/auth";
13724
+ var AUTH_ENDPOINT = "/api/v4/auth";
13723
13725
  var WITHDRAWAL_ENDPOINT = "/api/v4/orders/withdrawals";
13724
13726
  var MEMBERSHIP_ACTIVATION_ENDPOINT = "/api/v4/customer/memberships/activate";
13725
13727
  var ORDERS_ENDPOINT = "/api/v4/orders";
13726
13728
  var CUSTOMER_ADDRESSES_ENDPOINT = "/api/v4/customer/customer_addresses";
13727
13729
  var CUSTOMER_MEMBERSHIPS_ENDPOINT = "/api/v4/customer/memberships";
13730
+ var CUSTOMER_ADDRESS_ENDPOINT = "/api/v4/customer/customer_addresses/{id}";
13728
13731
  //#endregion
13729
13732
  //#region ../../packages/gomus-api/lib/customerLevels.ts
13730
13733
  var CustomerLevels = {
@@ -13746,6 +13749,8 @@ var Shop = class {
13746
13749
  locales: []
13747
13750
  }));
13748
13751
  #fetchStatus = {};
13752
+ #invalidationGens = {};
13753
+ #collectReads = null;
13749
13754
  constructor(apiUrl, shopDomain, locale, type = "angular") {
13750
13755
  this.type = type;
13751
13756
  if (apiUrl && shopDomain && locale) this.load(apiUrl, shopDomain, locale, type);
@@ -13865,6 +13870,66 @@ var Shop = class {
13865
13870
  if (!this.auth.data.accessToken) return NOT_SIGNED_IN;
13866
13871
  return this.fetchAndCache(CUSTOMER_MEMBERSHIPS_ENDPOINT, "customerMemberships", "", { cache: 5 });
13867
13872
  }
13873
+ /**
13874
+ * Shared skeleton of every customer write: the no-local-token guard (a write
13875
+ * must never ride on a same-origin session cookie), the request itself, and
13876
+ * the invalidate-on-ok tail that makes the matching cached read refetch.
13877
+ * Pass `null` when no cached read depends on the written resource.
13878
+ */
13879
+ async #authedWrite(invalidates, call) {
13880
+ if (!this.auth.data.accessToken) return NOT_SIGNED_IN;
13881
+ const result = await call();
13882
+ if (invalidates && result?.response?.ok) this.invalidate(invalidates);
13883
+ return result;
13884
+ }
13885
+ /**
13886
+ * Creates a saved address for the signed-in customer, then drops the cached
13887
+ * address list so the next getCustomerAddresses() refetches.
13888
+ * The backend body is root-keyed ({ customer_address: {...} }) — rootKey makes
13889
+ * apiCall validate required fields on the inner params, because the backend's
13890
+ * 422 carries no body and would give integrators nothing actionable.
13891
+ */
13892
+ async createCustomerAddress(params) {
13893
+ return this.#authedWrite(CUSTOMER_ADDRESSES_ENDPOINT, () => this.apiPost(CUSTOMER_ADDRESSES_ENDPOINT, {
13894
+ body: { customer_address: params },
13895
+ requiredFields: [
13896
+ "street",
13897
+ "zip",
13898
+ "city"
13899
+ ],
13900
+ rootKey: "customer_address"
13901
+ }));
13902
+ }
13903
+ /**
13904
+ * Updates a saved address (partial body — the backend applies only the sent
13905
+ * fields), then drops the cached address list. No client-side requiredFields:
13906
+ * every field is optional on update.
13907
+ */
13908
+ async updateCustomerAddress(id, params) {
13909
+ return this.#authedWrite(CUSTOMER_ADDRESSES_ENDPOINT, () => this.apiPut(CUSTOMER_ADDRESS_ENDPOINT, {
13910
+ body: { customer_address: params },
13911
+ params: { path: { id } }
13912
+ }));
13913
+ }
13914
+ /**
13915
+ * Deletes a saved address, then drops the cached address list. Success is
13916
+ * 204 No Content — parseAs 'text' skips openapi-fetch's JSON parse (see apiCall).
13917
+ */
13918
+ async deleteCustomerAddress(id) {
13919
+ return this.#authedWrite(CUSTOMER_ADDRESSES_ENDPOINT, () => this.apiDELETE(CUSTOMER_ADDRESS_ENDPOINT, {
13920
+ params: { path: { id } },
13921
+ parseAs: "text"
13922
+ }));
13923
+ }
13924
+ /**
13925
+ * Updates the signed-in customer's profile (PUT /api/v4/auth — devise
13926
+ * registrations#update). Profile fields only: password changes go through
13927
+ * updatePassword, which enforces current_password. Invalidates the cached
13928
+ * validate_token payload so getCustomer() refetches the fresh profile.
13929
+ */
13930
+ async updateCustomer(params) {
13931
+ return this.#authedWrite(VALIDATE_TOKEN_ENDPOINT, () => this.apiPut(AUTH_ENDPOINT, { body: params }));
13932
+ }
13868
13933
  ticketsCalendar(params) {
13869
13934
  return this.fetchAndCache(TICKETS_CALENDAR_ENDPOINT, `ticketsCalendar-${JSON.stringify(params)}`, "data", {
13870
13935
  cache: 60,
@@ -13917,7 +13982,7 @@ var Shop = class {
13917
13982
  "terms"
13918
13983
  ];
13919
13984
  if (!asGuest) requiredFields.push("password", "password_confirmation");
13920
- return this.apiPost(SIGN_UP_ENDPOINT, {
13985
+ return this.apiPost(AUTH_ENDPOINT, {
13921
13986
  body: params,
13922
13987
  requiredFields
13923
13988
  });
@@ -13945,15 +14010,15 @@ var Shop = class {
13945
14010
  cache: 60
13946
14011
  });
13947
14012
  }
13948
- updatePassword(params) {
13949
- return this.apiPut("/api/v4/auth/password", {
14013
+ async updatePassword(params) {
14014
+ return this.#authedWrite(null, () => this.apiPut("/api/v4/auth/password", {
13950
14015
  body: params,
13951
14016
  requiredFields: [
13952
14017
  "current_password",
13953
14018
  "password",
13954
14019
  "password_confirmation"
13955
14020
  ]
13956
- });
14021
+ }));
13957
14022
  }
13958
14023
  passwordReset(params) {
13959
14024
  return this.apiPost("/api/v4/auth/password", {
@@ -14028,6 +14093,25 @@ var Shop = class {
14028
14093
  }
14029
14094
  });
14030
14095
  }
14096
+ /**
14097
+ * Drops every cached GET slot whose fetch id starts with the endpoint prefix,
14098
+ * so the next fetchAndCache read refetches. #data is left intact — consumers
14099
+ * keep rendering the last known value until the refetch lands (no flash of
14100
+ * empty). Write methods call this after mutating the corresponding resource.
14101
+ *
14102
+ * In-flight slots are marked instead of deleted: their response predates the
14103
+ * write, but deleting them would release waitForAllFetches early AND let
14104
+ * apiGet resurrect the slot as fresh when the stale response lands. apiGet
14105
+ * drops marked slots on completion, so the next read refetches.
14106
+ */
14107
+ invalidate(endpointPrefix) {
14108
+ for (const [fetchId, slot] of Object.entries(this.#fetchStatus)) {
14109
+ if (!fetchId.startsWith(endpointPrefix)) continue;
14110
+ if (slot.status === "fetching") slot.invalidated = true;
14111
+ else delete this.#fetchStatus[fetchId];
14112
+ this.#invalidationGens[fetchId] = (this.#invalidationGens[fetchId] ?? 0) + 1;
14113
+ }
14114
+ }
14031
14115
  #fetchId(endpoint, query, path = {}) {
14032
14116
  return endpoint + JSON.stringify(query) + JSON.stringify(path);
14033
14117
  }
@@ -14053,6 +14137,7 @@ var Shop = class {
14053
14137
  }
14054
14138
  const query = options.query;
14055
14139
  const fetchId = this.#fetchId(endpoint, query, options.path);
14140
+ this.#collectReads?.(fetchId);
14056
14141
  const isNotFetchedYet = !this.#fetchStatus[fetchId];
14057
14142
  const isCacheExpired = this.#fetchStatus[fetchId]?.fetchedAt < Date.now() - options.cache * 1e3;
14058
14143
  if (isNotFetchedYet || isCacheExpired) this.apiGet(endpoint, query, options.path).then((ret) => {
@@ -14163,8 +14248,8 @@ var Shop = class {
14163
14248
  }
14164
14249
  async apiCall(path, options) {
14165
14250
  this.#ensureApi();
14166
- const { body, params = {}, requiredFields, parseAs } = options;
14167
- const validationErrors = validateApiPostBody(body, requiredFields);
14251
+ const { body, params = {}, requiredFields, rootKey, parseAs } = options;
14252
+ const validationErrors = validateApiPostBody((rootKey ? body?.[rootKey] : body) ?? {}, requiredFields);
14168
14253
  if (Object.keys(validationErrors).length > 0) return { error: { errors: validationErrors } };
14169
14254
  const httpMethod = this.client[options.method];
14170
14255
  return await httpMethod(path, {
@@ -14178,8 +14263,28 @@ var Shop = class {
14178
14263
  * and returns the result of the method execution.
14179
14264
  */
14180
14265
  async asyncFetch(method) {
14181
- await this.waitForAllFetches(method());
14182
- return method();
14266
+ const seenGens = /* @__PURE__ */ new Map();
14267
+ const call = () => {
14268
+ const prev = this.#collectReads;
14269
+ this.#collectReads = (id) => {
14270
+ if (!seenGens.has(id)) seenGens.set(id, this.#invalidationGens[id] ?? 0);
14271
+ };
14272
+ try {
14273
+ return method();
14274
+ } finally {
14275
+ this.#collectReads = prev;
14276
+ }
14277
+ };
14278
+ const invalidatedSinceRead = () => [...seenGens].some(([id, gen]) => (this.#invalidationGens[id] ?? 0) !== gen);
14279
+ call();
14280
+ await this.waitForAllFetches();
14281
+ let result = call();
14282
+ while (invalidatedSinceRead()) {
14283
+ for (const id of seenGens.keys()) seenGens.set(id, this.#invalidationGens[id] ?? 0);
14284
+ await this.waitForAllFetches();
14285
+ result = call();
14286
+ }
14287
+ return result;
14183
14288
  }
14184
14289
  async waitForAllFetches(...variables) {
14185
14290
  while (Object.values(this.#fetchStatus).filter((f) => f.status === "fetching").length) await wait(10);
@@ -14195,7 +14300,8 @@ var Shop = class {
14195
14300
  if (query) params = assign(params, { params: { query } });
14196
14301
  if (path) params = assign(params, { params: { path: pathOptions } });
14197
14302
  const ret = await this.client.GET(path, params);
14198
- this.#fetchStatus[fetchId] = {
14303
+ if (this.#fetchStatus[fetchId]?.invalidated) delete this.#fetchStatus[fetchId];
14304
+ else this.#fetchStatus[fetchId] = {
14199
14305
  status: "completed",
14200
14306
  fetchedAt: Date.now()
14201
14307
  };
@@ -37564,10 +37670,13 @@ function Password($$anchor, $$props) {
37564
37670
  async function submit(event) {
37565
37671
  const details = event.target.details;
37566
37672
  const result = await shop.updatePassword(details.formData);
37567
- if (result.data) {
37673
+ if ("data" in result && result.data) {
37568
37674
  details.successMessage = shop.t("user.passwordSuccess.desc.title");
37569
37675
  details.apiErrors = [];
37570
- } else details.apiErrors = result.error?.errors || result.error || result.errors;
37676
+ } else {
37677
+ const errors = ("error" in result ? result.error : void 0)?.errors ?? [];
37678
+ details.apiErrors = Array.isArray(errors) ? errors.map((e) => shop.t(e)) : errors;
37679
+ }
37571
37680
  }
37572
37681
  init();
37573
37682
  var go_form = root$6();
@@ -39147,6 +39256,30 @@ var go = {
39147
39256
  getCustomerMemberships: async () => {
39148
39257
  await ensureShopReady();
39149
39258
  return shop.asyncFetch(() => shop.getCustomerMemberships());
39259
+ },
39260
+ createCustomerAddress: async (params) => {
39261
+ await ensureShopReady();
39262
+ return shop.createCustomerAddress(params);
39263
+ },
39264
+ updateCustomerAddress: async (id, params) => {
39265
+ await ensureShopReady();
39266
+ return shop.updateCustomerAddress(id, params);
39267
+ },
39268
+ deleteCustomerAddress: async (id) => {
39269
+ await ensureShopReady();
39270
+ return shop.deleteCustomerAddress(id);
39271
+ },
39272
+ updateCustomer: async (params) => {
39273
+ await ensureShopReady();
39274
+ return shop.updateCustomer(params);
39275
+ },
39276
+ updatePassword: async (params) => {
39277
+ await ensureShopReady();
39278
+ return shop.updatePassword(params);
39279
+ },
39280
+ requestPasswordReset: async (params) => {
39281
+ await ensureShopReady();
39282
+ return shop.passwordReset(params);
39150
39283
  }
39151
39284
  },
39152
39285
  cart: { addItem: async (options) => {