@wix/himalaya-cli 0.1573.0 → 0.1575.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 (112) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-config.d.ts +16 -5
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/define-app.d.ts +6 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/message-format.d.ts +59 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/telemetry/wix-bi/src/index.d.ts +15 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/telemetry/wix-bi/src/oneapp.d.ts +90 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +3 -3
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/l10n.mjs +8 -3
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +3 -3
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-bi.mjs +165 -5
  10. package/dist/cli.mjs +5261 -2937
  11. package/dist/preview-assets.generated.bin +0 -0
  12. package/guides/designing-himi-screens.md +7 -1
  13. package/guides/hhp.md +2 -2
  14. package/guides/himi-json-output.md +53 -2
  15. package/guides/himi-mcp.md +1 -1
  16. package/guides/himi-test.md +3 -2
  17. package/guides/iap-products.md +1 -1
  18. package/guides/importing-oneapp-translations.md +115 -0
  19. package/guides/index.json +6 -0
  20. package/guides/one-app-section-mappings.md +16 -4
  21. package/guides/one-app-to-himi-app.md +167 -10
  22. package/guides/push-cli-onboarding.md +10 -0
  23. package/guides/qa-builds.md +21 -1
  24. package/guides/templates.md +3 -2
  25. package/guides/testing-himi-apps.md +1 -1
  26. package/package.json +3 -3
  27. package/templates/beauty-salon/dev/index.ts +96 -133
  28. package/templates/beauty-salon/tier5-src/_shared/exclusive-writes.ts +66 -0
  29. package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  30. package/templates/beauty-salon-owner/dev/index.ts +27 -6
  31. package/templates/beauty-salon-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  32. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  33. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  34. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  35. package/templates/branded-lite/dev/index.ts +136 -142
  36. package/templates/branded-lite/l10n/en.json +570 -0
  37. package/templates/branded-lite/l10n/he.json +200 -0
  38. package/templates/branded-lite/tier5-src/_shared/business-chat-copy.ts +40 -0
  39. package/templates/branded-lite/tier5-src/_shared/business-chat-message-model.ts +1 -1
  40. package/templates/branded-lite/tier5-src/_shared/business-chat-model.ts +3 -2
  41. package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +26 -21
  42. package/templates/branded-lite/tier5-src/_shared/exclusive-writes.ts +66 -0
  43. package/templates/branded-lite/tier5-src/business-chat/index.ts +184 -63
  44. package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  45. package/templates/community-club/dev/index.ts +22 -6
  46. package/templates/community-club-owner/dev/index.ts +27 -6
  47. package/templates/community-club-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  48. package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  49. package/templates/community-club-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  50. package/templates/community-club-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  51. package/templates/events-meetups/dev/index.ts +22 -6
  52. package/templates/events-meetups-owner/dev/index.ts +25 -6
  53. package/templates/events-meetups-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  54. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  55. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  56. package/templates/gym-fitness/dev/index.ts +96 -133
  57. package/templates/gym-fitness/tier5-src/_shared/exclusive-writes.ts +66 -0
  58. package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  59. package/templates/gym-fitness-owner/dev/index.ts +28 -6
  60. package/templates/gym-fitness-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  61. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  62. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  63. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  64. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  65. package/templates/himi-chat/dev/index.ts +22 -6
  66. package/templates/nonprofit-cause/dev/index.ts +22 -6
  67. package/templates/nonprofit-cause-owner/dev/index.ts +26 -6
  68. package/templates/nonprofit-cause-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  69. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  70. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  71. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  72. package/templates/owner-lite/dev/index.ts +28 -6
  73. package/templates/owner-lite/tier5-src/_shared/exclusive-writes.ts +66 -0
  74. package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  75. package/templates/owner-lite/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  76. package/templates/owner-lite/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  77. package/templates/owner-lite/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  78. package/templates/podcast-creator/dev/index.ts +22 -6
  79. package/templates/podcast-creator-owner/dev/index.ts +27 -6
  80. package/templates/podcast-creator-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  81. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  82. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  83. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  84. package/templates/portfolio-creative/dev/index.ts +96 -133
  85. package/templates/portfolio-creative/tier5-src/_shared/exclusive-writes.ts +66 -0
  86. package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  87. package/templates/portfolio-creative-owner/dev/index.ts +27 -6
  88. package/templates/portfolio-creative-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  89. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  90. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  91. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  92. package/templates/restaurant/dev/index.ts +22 -6
  93. package/templates/restaurant-owner/dev/index.ts +28 -6
  94. package/templates/restaurant-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  95. package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  96. package/templates/restaurant-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  97. package/templates/restaurant-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  98. package/templates/restaurant-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  99. package/templates/retail-store/dev/index.ts +96 -133
  100. package/templates/retail-store/tier5-src/_shared/exclusive-writes.ts +66 -0
  101. package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  102. package/templates/retail-store-owner/dev/index.ts +26 -6
  103. package/templates/retail-store-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  104. package/templates/retail-store-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  105. package/templates/retail-store-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  106. package/templates/surface-showcase/dev/index.ts +22 -6
  107. package/templates/yoga-studio/dev/index.ts +22 -6
  108. package/templates/yoga-studio-owner/dev/index.ts +26 -6
  109. package/templates/yoga-studio-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  110. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  111. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  112. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
@@ -6,7 +6,9 @@
6
6
  // form can't safely collect — so this form only edits an existing event's text
7
7
  // (title + description). Bound by buildForm ($service:himiOwnerLiteEventForm.*).
8
8
 
9
- import { defineService } from "@himalaya/state";
9
+ import { defineService as registerService } from "@himalaya/state";
10
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
11
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
10
12
  import { getEvent, updateEvent, type WixEvent } from "@himalaya/wix-events";
11
13
  import { normalizeEvent } from "../_shared/himi-events.js";
12
14
  import { postBodyText, plainPostBody } from "../_shared/owner-post-content.js";
@@ -66,6 +68,7 @@ interface EventFormState {
66
68
  when: string;
67
69
  location: string;
68
70
  status: "idle" | "loading" | "saving" | "saved" | "error";
71
+ writeBusy: boolean;
69
72
  error: string;
70
73
  savedMessage: string;
71
74
  /** WS-C — canonical detail target (`himi_owner_lite_event`); `savedId` rides
@@ -140,6 +143,7 @@ function shape(s: Partial<EventFormState>): EventFormState {
140
143
  when: s.when ?? "—",
141
144
  location: s.location ?? "—",
142
145
  status: s.status ?? "idle",
146
+ writeBusy: s.writeBusy ?? false,
143
147
  error: s.error ?? "",
144
148
  savedMessage: s.savedMessage ?? "",
145
149
  // WS-C — canonical detail target + the saved entity's id as a separate nav
@@ -8,7 +8,9 @@ import { editorSubmitValue, queuedDocument, editorChanged, editorRunKeys, resetH
8
8
  import { ricosUploadService } from "@himalaya/wix-rich-content-editor";
9
9
  import { wixCall } from "@himalaya/wix-site-media";
10
10
  import { wixMediaUrl, WIX_DETAIL_IMAGE } from "../_shared/himi-store.js";
11
- import { defineService } from "@himalaya/state";
11
+ import { defineService as registerService } from "@himalaya/state";
12
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
13
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
12
14
  import { getDraftPost, createDraftPost, updateDraftPost, type WixDraftPostWrite, type WixRichContent } from "@himalaya/wix-blog";
13
15
  import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback, ownerWriteError } from "../_shared/owner-load-error.js";
14
16
  import { DEMO_POSTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
@@ -53,6 +55,7 @@ interface PostFormState {
53
55
  noCover: boolean;
54
56
  isFeatured: string;
55
57
  status: "idle" | "loading" | "saving" | "saved" | "error";
58
+ writeBusy: boolean;
56
59
  error: string;
57
60
  savedMessage: string;
58
61
  /** WS-C — canonical detail target (`himi_owner_lite_post`); `savedId` rides
@@ -128,6 +131,7 @@ function shape(s: Partial<PostFormState>): PostFormState {
128
131
  noCover: !s.coverUrl,
129
132
  isFeatured: s.isFeatured ?? "false",
130
133
  status: s.status ?? "idle",
134
+ writeBusy: s.writeBusy ?? false,
131
135
  error: s.error ?? "",
132
136
  savedMessage: s.savedMessage ?? "",
133
137
  // WS-C — canonical detail target + the saved entity's id as a separate nav
@@ -6,7 +6,9 @@
6
6
  // the WRITE shape needs `priceData: { price: <number>, currency }`. Bound by the
7
7
  // generic buildForm descriptor ($service:himiOwnerLiteProductForm.*).
8
8
 
9
- import { defineService } from "@himalaya/state";
9
+ import { defineService as registerService } from "@himalaya/state";
10
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
11
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
10
12
  import {
11
13
  getProduct,
12
14
  createProduct,
@@ -76,6 +78,7 @@ interface ProductFormState {
76
78
  descriptionNote: string;
77
79
  visible: string;
78
80
  status: "idle" | "loading" | "saving" | "saved" | "error";
81
+ writeBusy: boolean;
79
82
  error: string;
80
83
  savedMessage: string;
81
84
  /** WS-C — canonical detail target (`himi_owner_lite_product`); `savedId` rides
@@ -161,6 +164,7 @@ function shape(s: Partial<ProductFormState>): ProductFormState {
161
164
  descriptionNote: dMode === "locked" ? copyParam("ownerProductForm.richDescriptionNote", "This description has formatting or media. Edit it in your site dashboard to keep that content intact; name, price and SKU can be changed here.") : "",
162
165
  visible: s.visible ?? "true",
163
166
  status: s.status ?? "idle",
167
+ writeBusy: s.writeBusy ?? false,
164
168
  error: s.error ?? "",
165
169
  savedMessage: s.savedMessage ?? "",
166
170
  // WS-C — canonical detail target + the saved entity's id as a separate nav
@@ -5,7 +5,9 @@
5
5
  // updateService. Bound by buildForm ($service:himiOwnerLiteServiceForm.*).
6
6
 
7
7
  import { landsOnLive } from "../_shared/owner-editor-value.js";
8
- import { defineService } from "@himalaya/state";
8
+ import { defineService as registerService } from "@himalaya/state";
9
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
10
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
9
11
  import { getService, updateService, type WixService } from "@himalaya/wix-bookings";
10
12
  import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback, ownerWriteError } from "../_shared/owner-load-error.js";
11
13
  import { DEMO_SERVICES, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
@@ -31,6 +33,7 @@ interface ServiceFormState {
31
33
  description: string;
32
34
  category: string;
33
35
  status: "idle" | "loading" | "saving" | "saved" | "error";
36
+ writeBusy: boolean;
34
37
  error: string;
35
38
  savedMessage: string;
36
39
  /** WS-C — canonical detail target (`himi_owner_lite_service`); `savedId` rides
@@ -88,6 +91,7 @@ function shape(s: Partial<ServiceFormState>): ServiceFormState {
88
91
  description: s.description ?? "",
89
92
  category: s.category ?? "",
90
93
  status: s.status ?? "idle",
94
+ writeBusy: s.writeBusy ?? false,
91
95
  error: s.error ?? "",
92
96
  savedMessage: s.savedMessage ?? "",
93
97
  // WS-C — canonical detail target + the saved entity's id as a separate nav
@@ -1799,15 +1799,31 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
1799
1799
 
1800
1800
  // Live seed-id enumeration: a published release serves only EXACT screens, so
1801
1801
  // `himi push` bakes each templated detail route's CURRENT catalog ids as exact
1802
- // screens via `seedIds`. Anonymous visitor rung against this app's own clientId;
1803
- // best-effort (offline build → no baked details, preview still live-resolves).
1802
+ // screens via `seedIds`. Anonymous visitor rung against this app's own clientId — or,
1803
+ // for an app bound through the One App visitor identity (`visitorSource: "one-app"`,
1804
+ // no client), that site's One App visitor instance. Best-effort (offline build → no
1805
+ // baked details, preview still live-resolves).
1806
+ async function liveSeedToken(WIX: string): Promise<string | undefined> {
1807
+ const auth = (config as { auth?: { config?: { clientId?: string; visitorSource?: string; metaSiteId?: string } } }).auth?.config;
1808
+ // A One App binding boots as the site's One App visitor, so the seed reads as that visitor too —
1809
+ // even beside a member-login clientId, and never repointed by a stray local WIX_CLIENT_ID.
1810
+ const oneApp = auth?.visitorSource === "one-app";
1811
+ const clientId = oneApp ? "" : process.env.WIX_CLIENT_ID ?? auth?.clientId ?? "";
1812
+ if (clientId && !clientId.startsWith("YOUR_")) {
1813
+ const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
1814
+ return ((await mint.json()) as { access_token?: string }).access_token;
1815
+ }
1816
+ const msid = oneApp ? auth?.metaSiteId ?? "" : "";
1817
+ if (!/^[0-9a-fA-F-]{10,64}$/.test(msid)) return undefined;
1818
+ const mint = await fetch(`https://apps.wix.com/_api/one-app-session-web/v3/businesses/${msid}/signed-instance?signed_instance_type=Visitor`, { headers: { Accept: "application/json" }, redirect: "error" });
1819
+ const visitor = (await mint.json()) as { signedInstance?: string; signedInstanceType?: string };
1820
+ return visitor.signedInstanceType === "Visitor" ? visitor.signedInstance : undefined;
1821
+ }
1822
+
1804
1823
  async function liveSeedIds(spec: LiveSeed): Promise<string[]> {
1805
1824
  try {
1806
1825
  const WIX = "https://www.wixapis.com";
1807
- const clientId = process.env.WIX_CLIENT_ID ?? (config as { auth?: { config?: { clientId?: string } } }).auth?.config?.clientId ?? "";
1808
- if (!clientId || clientId.startsWith("YOUR_")) return [];
1809
- const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
1810
- const token = ((await mint.json()) as { access_token?: string }).access_token;
1826
+ const token = await liveSeedToken(WIX);
1811
1827
  if (!token) return [];
1812
1828
  const res = await fetch(WIX + spec.path, { method: "POST", headers: { Authorization: token, "Content-Type": "application/json" }, body: JSON.stringify(spec.body) });
1813
1829
  const json = (await res.json()) as Record<string, unknown>;
@@ -26721,15 +26721,31 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
26721
26721
 
26722
26722
  // Live seed-id enumeration: a published release serves only EXACT screens, so
26723
26723
  // `himi push` bakes each templated detail route's CURRENT catalog ids as exact
26724
- // screens via `seedIds`. Anonymous visitor rung against this app's own clientId;
26725
- // best-effort (offline build → no baked details, preview still live-resolves).
26724
+ // screens via `seedIds`. Anonymous visitor rung against this app's own clientId — or,
26725
+ // for an app bound through the One App visitor identity (`visitorSource: "one-app"`,
26726
+ // no client), that site's One App visitor instance. Best-effort (offline build → no
26727
+ // baked details, preview still live-resolves).
26728
+ async function liveSeedToken(WIX: string): Promise<string | undefined> {
26729
+ const auth = (config as { auth?: { config?: { clientId?: string; visitorSource?: string; metaSiteId?: string } } }).auth?.config;
26730
+ // A One App binding boots as the site's One App visitor, so the seed reads as that visitor too —
26731
+ // even beside a member-login clientId, and never repointed by a stray local WIX_CLIENT_ID.
26732
+ const oneApp = auth?.visitorSource === "one-app";
26733
+ const clientId = oneApp ? "" : process.env.WIX_CLIENT_ID ?? auth?.clientId ?? "";
26734
+ if (clientId && !clientId.startsWith("YOUR_")) {
26735
+ const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
26736
+ return ((await mint.json()) as { access_token?: string }).access_token;
26737
+ }
26738
+ const msid = oneApp ? auth?.metaSiteId ?? "" : "";
26739
+ if (!/^[0-9a-fA-F-]{10,64}$/.test(msid)) return undefined;
26740
+ const mint = await fetch(`https://apps.wix.com/_api/one-app-session-web/v3/businesses/${msid}/signed-instance?signed_instance_type=Visitor`, { headers: { Accept: "application/json" }, redirect: "error" });
26741
+ const visitor = (await mint.json()) as { signedInstance?: string; signedInstanceType?: string };
26742
+ return visitor.signedInstanceType === "Visitor" ? visitor.signedInstance : undefined;
26743
+ }
26744
+
26726
26745
  async function liveSeedIds(spec: LiveSeed): Promise<string[]> {
26727
26746
  try {
26728
26747
  const WIX = "https://www.wixapis.com";
26729
- const clientId = process.env.WIX_CLIENT_ID ?? (config as { auth?: { config?: { clientId?: string } } }).auth?.config?.clientId ?? "";
26730
- if (!clientId || clientId.startsWith("YOUR_")) return [];
26731
- const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
26732
- const token = ((await mint.json()) as { access_token?: string }).access_token;
26748
+ const token = await liveSeedToken(WIX);
26733
26749
  if (!token) return [];
26734
26750
  const res = await fetch(WIX + spec.path, { method: "POST", headers: { Authorization: token, "Content-Type": "application/json" }, body: JSON.stringify(spec.body) });
26735
26751
  const json = (await res.json()) as Record<string, unknown>;
@@ -76564,6 +76564,7 @@ const SCREENS = runtimeSetting({
76564
76564
  "title": "Create post",
76565
76565
  "style": "primary",
76566
76566
  "fullWidth": true,
76567
+ "disabledPath": "$service:himiOwnerLitePostForm.writeBusy",
76567
76568
  "action": {
76568
76569
  "actionId": "tier5.himiOwnerLitePostForm.submit"
76569
76570
  }
@@ -76923,6 +76924,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
76923
76924
  "title": "Save changes",
76924
76925
  "style": "primary",
76925
76926
  "fullWidth": true,
76927
+ "disabledPath": "$service:himiOwnerLiteServiceForm.writeBusy",
76926
76928
  "action": {
76927
76929
  "actionId": "tier5.himiOwnerLiteServiceForm.submit"
76928
76930
  }
@@ -77285,6 +77287,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
77285
77287
  "title": "Save changes",
77286
77288
  "style": "primary",
77287
77289
  "fullWidth": true,
77290
+ "disabledPath": "$service:himiOwnerLiteEventForm.writeBusy",
77288
77291
  "action": {
77289
77292
  "actionId": "tier5.himiOwnerLiteEventForm.submit"
77290
77293
  }
@@ -77608,6 +77611,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
77608
77611
  "title": "Save changes",
77609
77612
  "style": "primary",
77610
77613
  "fullWidth": true,
77614
+ "disabledPath": "$service:himiOwnerLitePostForm.writeBusy",
77611
77615
  "action": {
77612
77616
  "actionId": "tier5.himiOwnerLitePostForm.submit"
77613
77617
  }
@@ -82115,15 +82119,31 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
82115
82119
 
82116
82120
  // Live seed-id enumeration: a published release serves only EXACT screens, so
82117
82121
  // `himi push` bakes each templated detail route's CURRENT catalog ids as exact
82118
- // screens via `seedIds`. Anonymous visitor rung against this app's own clientId;
82119
- // best-effort (offline build → no baked details, preview still live-resolves).
82122
+ // screens via `seedIds`. Anonymous visitor rung against this app's own clientId — or,
82123
+ // for an app bound through the One App visitor identity (`visitorSource: "one-app"`,
82124
+ // no client), that site's One App visitor instance. Best-effort (offline build → no
82125
+ // baked details, preview still live-resolves).
82126
+ async function liveSeedToken(WIX: string): Promise<string | undefined> {
82127
+ const auth = (config as { auth?: { config?: { clientId?: string; visitorSource?: string; metaSiteId?: string } } }).auth?.config;
82128
+ // A One App binding boots as the site's One App visitor, so the seed reads as that visitor too —
82129
+ // even beside a member-login clientId, and never repointed by a stray local WIX_CLIENT_ID.
82130
+ const oneApp = auth?.visitorSource === "one-app";
82131
+ const clientId = oneApp ? "" : process.env.WIX_CLIENT_ID ?? auth?.clientId ?? "";
82132
+ if (clientId && !clientId.startsWith("YOUR_")) {
82133
+ const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
82134
+ return ((await mint.json()) as { access_token?: string }).access_token;
82135
+ }
82136
+ const msid = oneApp ? auth?.metaSiteId ?? "" : "";
82137
+ if (!/^[0-9a-fA-F-]{10,64}$/.test(msid)) return undefined;
82138
+ const mint = await fetch(`https://apps.wix.com/_api/one-app-session-web/v3/businesses/${msid}/signed-instance?signed_instance_type=Visitor`, { headers: { Accept: "application/json" }, redirect: "error" });
82139
+ const visitor = (await mint.json()) as { signedInstance?: string; signedInstanceType?: string };
82140
+ return visitor.signedInstanceType === "Visitor" ? visitor.signedInstance : undefined;
82141
+ }
82142
+
82120
82143
  async function liveSeedIds(spec: LiveSeed): Promise<string[]> {
82121
82144
  try {
82122
82145
  const WIX = "https://www.wixapis.com";
82123
- const clientId = process.env.WIX_CLIENT_ID ?? (config as { auth?: { config?: { clientId?: string } } }).auth?.config?.clientId ?? "";
82124
- if (!clientId || clientId.startsWith("YOUR_")) return [];
82125
- const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
82126
- const token = ((await mint.json()) as { access_token?: string }).access_token;
82146
+ const token = await liveSeedToken(WIX);
82127
82147
  if (!token) return [];
82128
82148
  const res = await fetch(WIX + spec.path, { method: "POST", headers: { Authorization: token, "Content-Type": "application/json" }, body: JSON.stringify(spec.body) });
82129
82149
  const json = (await res.json()) as Record<string, unknown>;
@@ -0,0 +1,66 @@
1
+ import { defineService as register, type ServiceDefinition } from '@himalaya/state';
2
+
3
+ /** Reject repeated taps until the accepted write's result is committed. A promise
4
+ * queue alone releases before the host applies the reducer result, so the next
5
+ * write can still read an old revision/id. The private synchronous reducer below
6
+ * publishes busy and commits completion through the ordinary service handle;
7
+ * awaiting that handle guarantees the host has stored the result before unlock.
8
+ * Skipped actions return undefined, so they cannot roll back the accepted write.
9
+ */
10
+ export function withExclusiveWrites<S extends object = Record<string, unknown>>(
11
+ registerService: typeof register, names: readonly string[],
12
+ options: { prepare?: (action: string, args: readonly unknown[]) => void; coalesce?: readonly string[]; settle?: (before: S, next: S, live: S) => S } = {},
13
+ ): typeof register {
14
+ return ((name: string, definition: ServiceDefinition<Record<string, unknown>>) => {
15
+ let busy = false;
16
+ let activeAction = "";
17
+ let completion = Promise.resolve();
18
+ let complete = () => {};
19
+ let handle: { getState(): Record<string, unknown>; exclusiveWriteState(next: Record<string, unknown>): Promise<unknown> };
20
+ const wrapped: ServiceDefinition<Record<string, unknown>> = {
21
+ ...definition,
22
+ state: { ...definition.state, writeBusy: false },
23
+ notHydrated: [...(definition.notHydrated ?? []), 'writeBusy'],
24
+ actions: {
25
+ ...Object.fromEntries(Object.entries(definition.actions ?? {}).map(([action, reduce]) => [action,
26
+ names.includes(action) ? async (state: Record<string, unknown>, ...args: never[]) => {
27
+ // Intake request metadata even when its state replacement is skipped.
28
+ options.prepare?.(action, args);
29
+ if (busy) {
30
+ // Repeated initial loads must await the accepted load, so callers
31
+ // observe its committed rows rather than the initial skeleton.
32
+ if (activeAction === action && options.coalesce?.includes(action)) await completion;
33
+ return;
34
+ }
35
+ busy = true;
36
+ activeAction = action;
37
+ completion = new Promise<void>(resolve => { complete = resolve; });
38
+ try {
39
+ await handle.exclusiveWriteState({ ...handle.getState(), writeBusy: true });
40
+ const next = await reduce(state, ...args);
41
+ const live = handle.getState();
42
+ // Non-write actions (for example editing a draft field) can still run
43
+ // during I/O. Let the owning service preserve those newer edits.
44
+ const settled = options.settle ? options.settle(state as S, (next ?? live) as S, live as S) : next ?? live;
45
+ await handle.exclusiveWriteState({ ...settled, writeBusy: false });
46
+ } catch (error) {
47
+ await handle.exclusiveWriteState({ ...handle.getState(), writeBusy: false });
48
+ throw error;
49
+ } finally {
50
+ busy = false;
51
+ complete();
52
+ }
53
+ } : reduce,
54
+ ])),
55
+ exclusiveWriteState: (_state, next: Record<string, unknown>) => next,
56
+ },
57
+ };
58
+ const registered = registerService(name, wrapped);
59
+ // Registration preserves this private action, but the dynamic action map
60
+ // cannot express its payload to the generic service handle.
61
+ handle = registered as unknown as typeof handle;
62
+ // Older hosts ignore notHydrated. A fresh worker has no pending write.
63
+ handle.getState().writeBusy = false;
64
+ return registered;
65
+ }) as typeof register;
66
+ }
@@ -6,7 +6,9 @@
6
6
  // form can't safely collect — so this form only edits an existing event's text
7
7
  // (title + description). Bound by buildForm ($service:himiOwnerLiteEventForm.*).
8
8
 
9
- import { defineService } from "@himalaya/state";
9
+ import { defineService as registerService } from "@himalaya/state";
10
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
11
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
10
12
  import { getEvent, updateEvent, type WixEvent } from "@himalaya/wix-events";
11
13
  import { normalizeEvent } from "../_shared/himi-events.js";
12
14
  import { postBodyText, plainPostBody } from "../_shared/owner-post-content.js";
@@ -66,6 +68,7 @@ interface EventFormState {
66
68
  when: string;
67
69
  location: string;
68
70
  status: "idle" | "loading" | "saving" | "saved" | "error";
71
+ writeBusy: boolean;
69
72
  error: string;
70
73
  savedMessage: string;
71
74
  /** WS-C — canonical detail target (`himi_owner_lite_event`); `savedId` rides
@@ -140,6 +143,7 @@ function shape(s: Partial<EventFormState>): EventFormState {
140
143
  when: s.when ?? "—",
141
144
  location: s.location ?? "—",
142
145
  status: s.status ?? "idle",
146
+ writeBusy: s.writeBusy ?? false,
143
147
  error: s.error ?? "",
144
148
  savedMessage: s.savedMessage ?? "",
145
149
  // WS-C — canonical detail target + the saved entity's id as a separate nav
@@ -8,7 +8,9 @@ import { editorSubmitValue, queuedDocument, editorChanged, editorRunKeys, resetH
8
8
  import { ricosUploadService } from "@himalaya/wix-rich-content-editor";
9
9
  import { wixCall } from "@himalaya/wix-site-media";
10
10
  import { wixMediaUrl, WIX_DETAIL_IMAGE } from "../_shared/himi-store.js";
11
- import { defineService } from "@himalaya/state";
11
+ import { defineService as registerService } from "@himalaya/state";
12
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
13
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
12
14
  import { getDraftPost, createDraftPost, updateDraftPost, type WixDraftPostWrite, type WixRichContent } from "@himalaya/wix-blog";
13
15
  import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback, ownerWriteError } from "../_shared/owner-load-error.js";
14
16
  import { DEMO_POSTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
@@ -53,6 +55,7 @@ interface PostFormState {
53
55
  noCover: boolean;
54
56
  isFeatured: string;
55
57
  status: "idle" | "loading" | "saving" | "saved" | "error";
58
+ writeBusy: boolean;
56
59
  error: string;
57
60
  savedMessage: string;
58
61
  /** WS-C — canonical detail target (`himi_owner_lite_post`); `savedId` rides
@@ -128,6 +131,7 @@ function shape(s: Partial<PostFormState>): PostFormState {
128
131
  noCover: !s.coverUrl,
129
132
  isFeatured: s.isFeatured ?? "false",
130
133
  status: s.status ?? "idle",
134
+ writeBusy: s.writeBusy ?? false,
131
135
  error: s.error ?? "",
132
136
  savedMessage: s.savedMessage ?? "",
133
137
  // WS-C — canonical detail target + the saved entity's id as a separate nav
@@ -5,7 +5,9 @@
5
5
  // updateService. Bound by buildForm ($service:himiOwnerLiteServiceForm.*).
6
6
 
7
7
  import { landsOnLive } from "../_shared/owner-editor-value.js";
8
- import { defineService } from "@himalaya/state";
8
+ import { defineService as registerService } from "@himalaya/state";
9
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
10
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
9
11
  import { getService, updateService, type WixService } from "@himalaya/wix-bookings";
10
12
  import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback, ownerWriteError } from "../_shared/owner-load-error.js";
11
13
  import { DEMO_SERVICES, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
@@ -31,6 +33,7 @@ interface ServiceFormState {
31
33
  description: string;
32
34
  category: string;
33
35
  status: "idle" | "loading" | "saving" | "saved" | "error";
36
+ writeBusy: boolean;
34
37
  error: string;
35
38
  savedMessage: string;
36
39
  /** WS-C — canonical detail target (`himi_owner_lite_service`); `savedId` rides
@@ -88,6 +91,7 @@ function shape(s: Partial<ServiceFormState>): ServiceFormState {
88
91
  description: s.description ?? "",
89
92
  category: s.category ?? "",
90
93
  status: s.status ?? "idle",
94
+ writeBusy: s.writeBusy ?? false,
91
95
  error: s.error ?? "",
92
96
  savedMessage: s.savedMessage ?? "",
93
97
  // WS-C — canonical detail target + the saved entity's id as a separate nav
@@ -84172,6 +84172,7 @@ const SCREENS = runtimeSetting({
84172
84172
  "title": "Create product",
84173
84173
  "style": "primary",
84174
84174
  "fullWidth": true,
84175
+ "disabledPath": "$service:himiOwnerLiteProductForm.writeBusy",
84175
84176
  "action": {
84176
84177
  "actionId": "tier5.himiOwnerLiteProductForm.submit"
84177
84178
  }
@@ -84491,6 +84492,7 @@ const SCREENS = runtimeSetting({
84491
84492
  "title": "Create post",
84492
84493
  "style": "primary",
84493
84494
  "fullWidth": true,
84495
+ "disabledPath": "$service:himiOwnerLitePostForm.writeBusy",
84494
84496
  "action": {
84495
84497
  "actionId": "tier5.himiOwnerLitePostForm.submit"
84496
84498
  }
@@ -85006,6 +85008,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
85006
85008
  "title": "Save changes",
85007
85009
  "style": "primary",
85008
85010
  "fullWidth": true,
85011
+ "disabledPath": "$service:himiOwnerLiteProductForm.writeBusy",
85009
85012
  "action": {
85010
85013
  "actionId": "tier5.himiOwnerLiteProductForm.submit"
85011
85014
  }
@@ -85253,6 +85256,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
85253
85256
  "title": "Save changes",
85254
85257
  "style": "primary",
85255
85258
  "fullWidth": true,
85259
+ "disabledPath": "$service:himiOwnerLiteServiceForm.writeBusy",
85256
85260
  "action": {
85257
85261
  "actionId": "tier5.himiOwnerLiteServiceForm.submit"
85258
85262
  }
@@ -85615,6 +85619,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
85615
85619
  "title": "Save changes",
85616
85620
  "style": "primary",
85617
85621
  "fullWidth": true,
85622
+ "disabledPath": "$service:himiOwnerLiteEventForm.writeBusy",
85618
85623
  "action": {
85619
85624
  "actionId": "tier5.himiOwnerLiteEventForm.submit"
85620
85625
  }
@@ -85938,6 +85943,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
85938
85943
  "title": "Save changes",
85939
85944
  "style": "primary",
85940
85945
  "fullWidth": true,
85946
+ "disabledPath": "$service:himiOwnerLitePostForm.writeBusy",
85941
85947
  "action": {
85942
85948
  "actionId": "tier5.himiOwnerLitePostForm.submit"
85943
85949
  }
@@ -93008,15 +93014,31 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
93008
93014
 
93009
93015
  // Live seed-id enumeration: a published release serves only EXACT screens, so
93010
93016
  // `himi push` bakes each templated detail route's CURRENT catalog ids as exact
93011
- // screens via `seedIds`. Anonymous visitor rung against this app's own clientId;
93012
- // best-effort (offline build → no baked details, preview still live-resolves).
93017
+ // screens via `seedIds`. Anonymous visitor rung against this app's own clientId — or,
93018
+ // for an app bound through the One App visitor identity (`visitorSource: "one-app"`,
93019
+ // no client), that site's One App visitor instance. Best-effort (offline build → no
93020
+ // baked details, preview still live-resolves).
93021
+ async function liveSeedToken(WIX: string): Promise<string | undefined> {
93022
+ const auth = (config as { auth?: { config?: { clientId?: string; visitorSource?: string; metaSiteId?: string } } }).auth?.config;
93023
+ // A One App binding boots as the site's One App visitor, so the seed reads as that visitor too —
93024
+ // even beside a member-login clientId, and never repointed by a stray local WIX_CLIENT_ID.
93025
+ const oneApp = auth?.visitorSource === "one-app";
93026
+ const clientId = oneApp ? "" : process.env.WIX_CLIENT_ID ?? auth?.clientId ?? "";
93027
+ if (clientId && !clientId.startsWith("YOUR_")) {
93028
+ const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
93029
+ return ((await mint.json()) as { access_token?: string }).access_token;
93030
+ }
93031
+ const msid = oneApp ? auth?.metaSiteId ?? "" : "";
93032
+ if (!/^[0-9a-fA-F-]{10,64}$/.test(msid)) return undefined;
93033
+ const mint = await fetch(`https://apps.wix.com/_api/one-app-session-web/v3/businesses/${msid}/signed-instance?signed_instance_type=Visitor`, { headers: { Accept: "application/json" }, redirect: "error" });
93034
+ const visitor = (await mint.json()) as { signedInstance?: string; signedInstanceType?: string };
93035
+ return visitor.signedInstanceType === "Visitor" ? visitor.signedInstance : undefined;
93036
+ }
93037
+
93013
93038
  async function liveSeedIds(spec: LiveSeed): Promise<string[]> {
93014
93039
  try {
93015
93040
  const WIX = "https://www.wixapis.com";
93016
- const clientId = process.env.WIX_CLIENT_ID ?? (config as { auth?: { config?: { clientId?: string } } }).auth?.config?.clientId ?? "";
93017
- if (!clientId || clientId.startsWith("YOUR_")) return [];
93018
- const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
93019
- const token = ((await mint.json()) as { access_token?: string }).access_token;
93041
+ const token = await liveSeedToken(WIX);
93020
93042
  if (!token) return [];
93021
93043
  const res = await fetch(WIX + spec.path, { method: "POST", headers: { Authorization: token, "Content-Type": "application/json" }, body: JSON.stringify(spec.body) });
93022
93044
  const json = (await res.json()) as Record<string, unknown>;
@@ -0,0 +1,66 @@
1
+ import { defineService as register, type ServiceDefinition } from '@himalaya/state';
2
+
3
+ /** Reject repeated taps until the accepted write's result is committed. A promise
4
+ * queue alone releases before the host applies the reducer result, so the next
5
+ * write can still read an old revision/id. The private synchronous reducer below
6
+ * publishes busy and commits completion through the ordinary service handle;
7
+ * awaiting that handle guarantees the host has stored the result before unlock.
8
+ * Skipped actions return undefined, so they cannot roll back the accepted write.
9
+ */
10
+ export function withExclusiveWrites<S extends object = Record<string, unknown>>(
11
+ registerService: typeof register, names: readonly string[],
12
+ options: { prepare?: (action: string, args: readonly unknown[]) => void; coalesce?: readonly string[]; settle?: (before: S, next: S, live: S) => S } = {},
13
+ ): typeof register {
14
+ return ((name: string, definition: ServiceDefinition<Record<string, unknown>>) => {
15
+ let busy = false;
16
+ let activeAction = "";
17
+ let completion = Promise.resolve();
18
+ let complete = () => {};
19
+ let handle: { getState(): Record<string, unknown>; exclusiveWriteState(next: Record<string, unknown>): Promise<unknown> };
20
+ const wrapped: ServiceDefinition<Record<string, unknown>> = {
21
+ ...definition,
22
+ state: { ...definition.state, writeBusy: false },
23
+ notHydrated: [...(definition.notHydrated ?? []), 'writeBusy'],
24
+ actions: {
25
+ ...Object.fromEntries(Object.entries(definition.actions ?? {}).map(([action, reduce]) => [action,
26
+ names.includes(action) ? async (state: Record<string, unknown>, ...args: never[]) => {
27
+ // Intake request metadata even when its state replacement is skipped.
28
+ options.prepare?.(action, args);
29
+ if (busy) {
30
+ // Repeated initial loads must await the accepted load, so callers
31
+ // observe its committed rows rather than the initial skeleton.
32
+ if (activeAction === action && options.coalesce?.includes(action)) await completion;
33
+ return;
34
+ }
35
+ busy = true;
36
+ activeAction = action;
37
+ completion = new Promise<void>(resolve => { complete = resolve; });
38
+ try {
39
+ await handle.exclusiveWriteState({ ...handle.getState(), writeBusy: true });
40
+ const next = await reduce(state, ...args);
41
+ const live = handle.getState();
42
+ // Non-write actions (for example editing a draft field) can still run
43
+ // during I/O. Let the owning service preserve those newer edits.
44
+ const settled = options.settle ? options.settle(state as S, (next ?? live) as S, live as S) : next ?? live;
45
+ await handle.exclusiveWriteState({ ...settled, writeBusy: false });
46
+ } catch (error) {
47
+ await handle.exclusiveWriteState({ ...handle.getState(), writeBusy: false });
48
+ throw error;
49
+ } finally {
50
+ busy = false;
51
+ complete();
52
+ }
53
+ } : reduce,
54
+ ])),
55
+ exclusiveWriteState: (_state, next: Record<string, unknown>) => next,
56
+ },
57
+ };
58
+ const registered = registerService(name, wrapped);
59
+ // Registration preserves this private action, but the dynamic action map
60
+ // cannot express its payload to the generic service handle.
61
+ handle = registered as unknown as typeof handle;
62
+ // Older hosts ignore notHydrated. A fresh worker has no pending write.
63
+ handle.getState().writeBusy = false;
64
+ return registered;
65
+ }) as typeof register;
66
+ }
@@ -6,7 +6,9 @@
6
6
  // form can't safely collect — so this form only edits an existing event's text
7
7
  // (title + description). Bound by buildForm ($service:himiOwnerLiteEventForm.*).
8
8
 
9
- import { defineService } from "@himalaya/state";
9
+ import { defineService as registerService } from "@himalaya/state";
10
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
11
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
10
12
  import { getEvent, updateEvent, type WixEvent } from "@himalaya/wix-events";
11
13
  import { normalizeEvent } from "../_shared/himi-events.js";
12
14
  import { postBodyText, plainPostBody } from "../_shared/owner-post-content.js";
@@ -66,6 +68,7 @@ interface EventFormState {
66
68
  when: string;
67
69
  location: string;
68
70
  status: "idle" | "loading" | "saving" | "saved" | "error";
71
+ writeBusy: boolean;
69
72
  error: string;
70
73
  savedMessage: string;
71
74
  /** WS-C — canonical detail target (`himi_owner_lite_event`); `savedId` rides
@@ -140,6 +143,7 @@ function shape(s: Partial<EventFormState>): EventFormState {
140
143
  when: s.when ?? "—",
141
144
  location: s.location ?? "—",
142
145
  status: s.status ?? "idle",
146
+ writeBusy: s.writeBusy ?? false,
143
147
  error: s.error ?? "",
144
148
  savedMessage: s.savedMessage ?? "",
145
149
  // WS-C — canonical detail target + the saved entity's id as a separate nav
@@ -8,7 +8,9 @@ import { editorSubmitValue, queuedDocument, editorChanged, editorRunKeys, resetH
8
8
  import { ricosUploadService } from "@himalaya/wix-rich-content-editor";
9
9
  import { wixCall } from "@himalaya/wix-site-media";
10
10
  import { wixMediaUrl, WIX_DETAIL_IMAGE } from "../_shared/himi-store.js";
11
- import { defineService } from "@himalaya/state";
11
+ import { defineService as registerService } from "@himalaya/state";
12
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
13
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
12
14
  import { getDraftPost, createDraftPost, updateDraftPost, type WixDraftPostWrite, type WixRichContent } from "@himalaya/wix-blog";
13
15
  import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback, ownerWriteError } from "../_shared/owner-load-error.js";
14
16
  import { DEMO_POSTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
@@ -53,6 +55,7 @@ interface PostFormState {
53
55
  noCover: boolean;
54
56
  isFeatured: string;
55
57
  status: "idle" | "loading" | "saving" | "saved" | "error";
58
+ writeBusy: boolean;
56
59
  error: string;
57
60
  savedMessage: string;
58
61
  /** WS-C — canonical detail target (`himi_owner_lite_post`); `savedId` rides
@@ -128,6 +131,7 @@ function shape(s: Partial<PostFormState>): PostFormState {
128
131
  noCover: !s.coverUrl,
129
132
  isFeatured: s.isFeatured ?? "false",
130
133
  status: s.status ?? "idle",
134
+ writeBusy: s.writeBusy ?? false,
131
135
  error: s.error ?? "",
132
136
  savedMessage: s.savedMessage ?? "",
133
137
  // WS-C — canonical detail target + the saved entity's id as a separate nav