@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.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-config.d.ts +16 -5
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/define-app.d.ts +6 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/message-format.d.ts +59 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/telemetry/wix-bi/src/index.d.ts +15 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/telemetry/wix-bi/src/oneapp.d.ts +90 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +3 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/l10n.mjs +8 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +3 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-bi.mjs +165 -5
- package/dist/cli.mjs +5261 -2937
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/designing-himi-screens.md +7 -1
- package/guides/hhp.md +2 -2
- package/guides/himi-json-output.md +53 -2
- package/guides/himi-mcp.md +1 -1
- package/guides/himi-test.md +3 -2
- package/guides/iap-products.md +1 -1
- package/guides/importing-oneapp-translations.md +115 -0
- package/guides/index.json +6 -0
- package/guides/one-app-section-mappings.md +16 -4
- package/guides/one-app-to-himi-app.md +167 -10
- package/guides/push-cli-onboarding.md +10 -0
- package/guides/qa-builds.md +21 -1
- package/guides/templates.md +3 -2
- package/guides/testing-himi-apps.md +1 -1
- package/package.json +3 -3
- package/templates/beauty-salon/dev/index.ts +96 -133
- package/templates/beauty-salon/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +18 -5
- package/templates/beauty-salon-owner/dev/index.ts +27 -6
- package/templates/beauty-salon-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
- package/templates/branded-lite/dev/index.ts +136 -142
- package/templates/branded-lite/l10n/en.json +570 -0
- package/templates/branded-lite/l10n/he.json +200 -0
- package/templates/branded-lite/tier5-src/_shared/business-chat-copy.ts +40 -0
- package/templates/branded-lite/tier5-src/_shared/business-chat-message-model.ts +1 -1
- package/templates/branded-lite/tier5-src/_shared/business-chat-model.ts +3 -2
- package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +26 -21
- package/templates/branded-lite/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/branded-lite/tier5-src/business-chat/index.ts +184 -63
- package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +18 -5
- package/templates/community-club/dev/index.ts +22 -6
- package/templates/community-club-owner/dev/index.ts +27 -6
- package/templates/community-club-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
- package/templates/community-club-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
- package/templates/community-club-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
- package/templates/events-meetups/dev/index.ts +22 -6
- package/templates/events-meetups-owner/dev/index.ts +25 -6
- package/templates/events-meetups-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
- package/templates/gym-fitness/dev/index.ts +96 -133
- package/templates/gym-fitness/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +18 -5
- package/templates/gym-fitness-owner/dev/index.ts +28 -6
- package/templates/gym-fitness-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
- package/templates/himi-chat/dev/index.ts +22 -6
- package/templates/nonprofit-cause/dev/index.ts +22 -6
- package/templates/nonprofit-cause-owner/dev/index.ts +26 -6
- package/templates/nonprofit-cause-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
- package/templates/owner-lite/dev/index.ts +28 -6
- package/templates/owner-lite/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
- package/templates/owner-lite/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
- package/templates/owner-lite/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
- package/templates/owner-lite/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
- package/templates/podcast-creator/dev/index.ts +22 -6
- package/templates/podcast-creator-owner/dev/index.ts +27 -6
- package/templates/podcast-creator-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
- package/templates/portfolio-creative/dev/index.ts +96 -133
- package/templates/portfolio-creative/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +18 -5
- package/templates/portfolio-creative-owner/dev/index.ts +27 -6
- package/templates/portfolio-creative-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
- package/templates/restaurant/dev/index.ts +22 -6
- package/templates/restaurant-owner/dev/index.ts +28 -6
- package/templates/restaurant-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
- package/templates/retail-store/dev/index.ts +96 -133
- package/templates/retail-store/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +18 -5
- package/templates/retail-store-owner/dev/index.ts +26 -6
- package/templates/retail-store-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
- package/templates/surface-showcase/dev/index.ts +22 -6
- package/templates/yoga-studio/dev/index.ts +22 -6
- package/templates/yoga-studio-owner/dev/index.ts +26 -6
- package/templates/yoga-studio-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
|
@@ -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
|
|
@@ -26770,15 +26770,31 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
26770
26770
|
|
|
26771
26771
|
// Live seed-id enumeration: a published release serves only EXACT screens, so
|
|
26772
26772
|
// `himi push` bakes each templated detail route's CURRENT catalog ids as exact
|
|
26773
|
-
// screens via `seedIds`. Anonymous visitor rung against this app's own clientId
|
|
26774
|
-
//
|
|
26773
|
+
// screens via `seedIds`. Anonymous visitor rung against this app's own clientId — or,
|
|
26774
|
+
// for an app bound through the One App visitor identity (`visitorSource: "one-app"`,
|
|
26775
|
+
// no client), that site's One App visitor instance. Best-effort (offline build → no
|
|
26776
|
+
// baked details, preview still live-resolves).
|
|
26777
|
+
async function liveSeedToken(WIX: string): Promise<string | undefined> {
|
|
26778
|
+
const auth = (config as { auth?: { config?: { clientId?: string; visitorSource?: string; metaSiteId?: string } } }).auth?.config;
|
|
26779
|
+
// A One App binding boots as the site's One App visitor, so the seed reads as that visitor too —
|
|
26780
|
+
// even beside a member-login clientId, and never repointed by a stray local WIX_CLIENT_ID.
|
|
26781
|
+
const oneApp = auth?.visitorSource === "one-app";
|
|
26782
|
+
const clientId = oneApp ? "" : process.env.WIX_CLIENT_ID ?? auth?.clientId ?? "";
|
|
26783
|
+
if (clientId && !clientId.startsWith("YOUR_")) {
|
|
26784
|
+
const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
|
|
26785
|
+
return ((await mint.json()) as { access_token?: string }).access_token;
|
|
26786
|
+
}
|
|
26787
|
+
const msid = oneApp ? auth?.metaSiteId ?? "" : "";
|
|
26788
|
+
if (!/^[0-9a-fA-F-]{10,64}$/.test(msid)) return undefined;
|
|
26789
|
+
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" });
|
|
26790
|
+
const visitor = (await mint.json()) as { signedInstance?: string; signedInstanceType?: string };
|
|
26791
|
+
return visitor.signedInstanceType === "Visitor" ? visitor.signedInstance : undefined;
|
|
26792
|
+
}
|
|
26793
|
+
|
|
26775
26794
|
async function liveSeedIds(spec: LiveSeed): Promise<string[]> {
|
|
26776
26795
|
try {
|
|
26777
26796
|
const WIX = "https://www.wixapis.com";
|
|
26778
|
-
const
|
|
26779
|
-
if (!clientId || clientId.startsWith("YOUR_")) return [];
|
|
26780
|
-
const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
|
|
26781
|
-
const token = ((await mint.json()) as { access_token?: string }).access_token;
|
|
26797
|
+
const token = await liveSeedToken(WIX);
|
|
26782
26798
|
if (!token) return [];
|
|
26783
26799
|
const res = await fetch(WIX + spec.path, { method: "POST", headers: { Authorization: token, "Content-Type": "application/json" }, body: JSON.stringify(spec.body) });
|
|
26784
26800
|
const json = (await res.json()) as Record<string, unknown>;
|
|
@@ -78486,6 +78486,7 @@ const SCREENS = runtimeSetting({
|
|
|
78486
78486
|
"title": "Create product",
|
|
78487
78487
|
"style": "primary",
|
|
78488
78488
|
"fullWidth": true,
|
|
78489
|
+
"disabledPath": "$service:himiOwnerLiteProductForm.writeBusy",
|
|
78489
78490
|
"action": {
|
|
78490
78491
|
"actionId": "tier5.himiOwnerLiteProductForm.submit"
|
|
78491
78492
|
}
|
|
@@ -78805,6 +78806,7 @@ const SCREENS = runtimeSetting({
|
|
|
78805
78806
|
"title": "Create episode",
|
|
78806
78807
|
"style": "primary",
|
|
78807
78808
|
"fullWidth": true,
|
|
78809
|
+
"disabledPath": "$service:himiOwnerLitePostForm.writeBusy",
|
|
78808
78810
|
"action": {
|
|
78809
78811
|
"actionId": "tier5.himiOwnerLitePostForm.submit"
|
|
78810
78812
|
}
|
|
@@ -79321,6 +79323,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
79321
79323
|
"title": "Save changes",
|
|
79322
79324
|
"style": "primary",
|
|
79323
79325
|
"fullWidth": true,
|
|
79326
|
+
"disabledPath": "$service:himiOwnerLiteProductForm.writeBusy",
|
|
79324
79327
|
"action": {
|
|
79325
79328
|
"actionId": "tier5.himiOwnerLiteProductForm.submit"
|
|
79326
79329
|
}
|
|
@@ -79683,6 +79686,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
79683
79686
|
"title": "Save changes",
|
|
79684
79687
|
"style": "primary",
|
|
79685
79688
|
"fullWidth": true,
|
|
79689
|
+
"disabledPath": "$service:himiOwnerLiteEventForm.writeBusy",
|
|
79686
79690
|
"action": {
|
|
79687
79691
|
"actionId": "tier5.himiOwnerLiteEventForm.submit"
|
|
79688
79692
|
}
|
|
@@ -80006,6 +80010,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
80006
80010
|
"title": "Save changes",
|
|
80007
80011
|
"style": "primary",
|
|
80008
80012
|
"fullWidth": true,
|
|
80013
|
+
"disabledPath": "$service:himiOwnerLitePostForm.writeBusy",
|
|
80009
80014
|
"action": {
|
|
80010
80015
|
"actionId": "tier5.himiOwnerLitePostForm.submit"
|
|
80011
80016
|
}
|
|
@@ -85003,15 +85008,31 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
85003
85008
|
|
|
85004
85009
|
// Live seed-id enumeration: a published release serves only EXACT screens, so
|
|
85005
85010
|
// `himi push` bakes each templated detail route's CURRENT catalog ids as exact
|
|
85006
|
-
// screens via `seedIds`. Anonymous visitor rung against this app's own clientId
|
|
85007
|
-
//
|
|
85011
|
+
// screens via `seedIds`. Anonymous visitor rung against this app's own clientId — or,
|
|
85012
|
+
// for an app bound through the One App visitor identity (`visitorSource: "one-app"`,
|
|
85013
|
+
// no client), that site's One App visitor instance. Best-effort (offline build → no
|
|
85014
|
+
// baked details, preview still live-resolves).
|
|
85015
|
+
async function liveSeedToken(WIX: string): Promise<string | undefined> {
|
|
85016
|
+
const auth = (config as { auth?: { config?: { clientId?: string; visitorSource?: string; metaSiteId?: string } } }).auth?.config;
|
|
85017
|
+
// A One App binding boots as the site's One App visitor, so the seed reads as that visitor too —
|
|
85018
|
+
// even beside a member-login clientId, and never repointed by a stray local WIX_CLIENT_ID.
|
|
85019
|
+
const oneApp = auth?.visitorSource === "one-app";
|
|
85020
|
+
const clientId = oneApp ? "" : process.env.WIX_CLIENT_ID ?? auth?.clientId ?? "";
|
|
85021
|
+
if (clientId && !clientId.startsWith("YOUR_")) {
|
|
85022
|
+
const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
|
|
85023
|
+
return ((await mint.json()) as { access_token?: string }).access_token;
|
|
85024
|
+
}
|
|
85025
|
+
const msid = oneApp ? auth?.metaSiteId ?? "" : "";
|
|
85026
|
+
if (!/^[0-9a-fA-F-]{10,64}$/.test(msid)) return undefined;
|
|
85027
|
+
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" });
|
|
85028
|
+
const visitor = (await mint.json()) as { signedInstance?: string; signedInstanceType?: string };
|
|
85029
|
+
return visitor.signedInstanceType === "Visitor" ? visitor.signedInstance : undefined;
|
|
85030
|
+
}
|
|
85031
|
+
|
|
85008
85032
|
async function liveSeedIds(spec: LiveSeed): Promise<string[]> {
|
|
85009
85033
|
try {
|
|
85010
85034
|
const WIX = "https://www.wixapis.com";
|
|
85011
|
-
const
|
|
85012
|
-
if (!clientId || clientId.startsWith("YOUR_")) return [];
|
|
85013
|
-
const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
|
|
85014
|
-
const token = ((await mint.json()) as { access_token?: string }).access_token;
|
|
85035
|
+
const token = await liveSeedToken(WIX);
|
|
85015
85036
|
if (!token) return [];
|
|
85016
85037
|
const res = await fetch(WIX + spec.path, { method: "POST", headers: { Authorization: token, "Content-Type": "application/json" }, body: JSON.stringify(spec.body) });
|
|
85017
85038
|
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
|
|
@@ -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
|