@wix/himalaya-cli 0.1476.0 → 0.1477.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 (39) hide show
  1. package/package.json +2 -2
  2. package/templates/beauty-salon/tier5-src/_shared/engine-intl.ts +21 -0
  3. package/templates/beauty-salon/tier5-src/_shared/himi-bookings.ts +35 -0
  4. package/templates/beauty-salon/tier5-src/_shared/host-events.ts +45 -6
  5. package/templates/beauty-salon/tier5-src/himi-booking-bookings/index.ts +4 -0
  6. package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +34 -13
  7. package/templates/beauty-salon-owner/tier5-src/_shared/engine-intl.ts +21 -0
  8. package/templates/branded-lite/tier5-src/_shared/engine-intl.ts +21 -0
  9. package/templates/branded-lite/tier5-src/_shared/himi-bookings.ts +35 -0
  10. package/templates/branded-lite/tier5-src/_shared/host-events.ts +45 -6
  11. package/templates/branded-lite/tier5-src/himi-booking-bookings/index.ts +4 -0
  12. package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +34 -13
  13. package/templates/community-club/tier5-src/_shared/engine-intl.ts +21 -0
  14. package/templates/community-club-owner/tier5-src/_shared/engine-intl.ts +21 -0
  15. package/templates/events-meetups/tier5-src/_shared/engine-intl.ts +21 -0
  16. package/templates/events-meetups-owner/tier5-src/_shared/engine-intl.ts +21 -0
  17. package/templates/gym-fitness/tier5-src/_shared/engine-intl.ts +21 -0
  18. package/templates/gym-fitness/tier5-src/_shared/himi-bookings.ts +35 -0
  19. package/templates/gym-fitness/tier5-src/_shared/host-events.ts +45 -6
  20. package/templates/gym-fitness/tier5-src/himi-booking-bookings/index.ts +4 -0
  21. package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +34 -13
  22. package/templates/gym-fitness-owner/tier5-src/_shared/engine-intl.ts +21 -0
  23. package/templates/nonprofit-cause/tier5-src/_shared/engine-intl.ts +21 -0
  24. package/templates/nonprofit-cause-owner/tier5-src/_shared/engine-intl.ts +21 -0
  25. package/templates/owner-lite/tier5-src/_shared/engine-intl.ts +21 -0
  26. package/templates/podcast-creator/tier5-src/_shared/engine-intl.ts +21 -0
  27. package/templates/podcast-creator-owner/tier5-src/_shared/engine-intl.ts +21 -0
  28. package/templates/portfolio-creative/tier5-src/_shared/engine-intl.ts +21 -0
  29. package/templates/portfolio-creative-owner/tier5-src/_shared/engine-intl.ts +21 -0
  30. package/templates/restaurant/tier5-src/_shared/engine-intl.ts +21 -0
  31. package/templates/restaurant-owner/tier5-src/_shared/engine-intl.ts +21 -0
  32. package/templates/retail-store/tier5-src/_shared/engine-intl.ts +21 -0
  33. package/templates/retail-store-owner/tier5-src/_shared/engine-intl.ts +21 -0
  34. package/templates/yoga-studio/tier5-src/_shared/engine-intl.ts +21 -0
  35. package/templates/yoga-studio/tier5-src/_shared/himi-bookings.ts +35 -0
  36. package/templates/yoga-studio/tier5-src/_shared/host-events.ts +45 -6
  37. package/templates/yoga-studio/tier5-src/himi-booking-bookings/index.ts +4 -0
  38. package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +34 -13
  39. package/templates/yoga-studio-owner/tier5-src/_shared/engine-intl.ts +21 -0
@@ -444,3 +444,24 @@ export function calendarDay(date: Date, timeZone?: string): string {
444
444
  }
445
445
  return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
446
446
  }
447
+
448
+ /**
449
+ * [timeZone]'s offset from UTC in ms at instant [at] (Asia/Jerusalem in winter: 7_200_000), or NaN
450
+ * where it cannot be known — no ECMA-402 (the wrist engines) or a zone this engine does not know.
451
+ * Callers fall back to the device zone on NaN, as the formatters above do.
452
+ */
453
+ export function zoneOffsetAt(at: number, timeZone: string): number {
454
+ if (hasIntl() && Number.isFinite(at)) {
455
+ try {
456
+ const parts = new Intl.DateTimeFormat("en-US", {
457
+ timeZone, hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit",
458
+ hour: "2-digit", minute: "2-digit", second: "2-digit",
459
+ }).formatToParts(new Date(at));
460
+ const n = (type: string) => Number(parts.find((p) => p.type === type)?.value);
461
+ return Date.UTC(n("year"), n("month") - 1, n("day"), n("hour") % 24, n("minute"), n("second")) - at;
462
+ } catch {
463
+ /* a zone this engine does not know: NaN below */
464
+ }
465
+ }
466
+ return NaN;
467
+ }
@@ -444,3 +444,24 @@ export function calendarDay(date: Date, timeZone?: string): string {
444
444
  }
445
445
  return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
446
446
  }
447
+
448
+ /**
449
+ * [timeZone]'s offset from UTC in ms at instant [at] (Asia/Jerusalem in winter: 7_200_000), or NaN
450
+ * where it cannot be known — no ECMA-402 (the wrist engines) or a zone this engine does not know.
451
+ * Callers fall back to the device zone on NaN, as the formatters above do.
452
+ */
453
+ export function zoneOffsetAt(at: number, timeZone: string): number {
454
+ if (hasIntl() && Number.isFinite(at)) {
455
+ try {
456
+ const parts = new Intl.DateTimeFormat("en-US", {
457
+ timeZone, hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit",
458
+ hour: "2-digit", minute: "2-digit", second: "2-digit",
459
+ }).formatToParts(new Date(at));
460
+ const n = (type: string) => Number(parts.find((p) => p.type === type)?.value);
461
+ return Date.UTC(n("year"), n("month") - 1, n("day"), n("hour") % 24, n("minute"), n("second")) - at;
462
+ } catch {
463
+ /* a zone this engine does not know: NaN below */
464
+ }
465
+ }
466
+ return NaN;
467
+ }
@@ -16,3 +16,38 @@ export function categoryName(value: unknown): string {
16
16
  const name = (value as { name?: unknown }).name;
17
17
  return typeof name === "string" ? name.trim() : "";
18
18
  }
19
+
20
+ import { zoneOffsetAt } from "./engine-intl.js";
21
+
22
+ const HAS_OFFSET = /(?:Z|[+-]\d{2}:?\d{2})$/i;
23
+ const LOCAL_TIME = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?/;
24
+
25
+ /** The instant (ms) a slot starts. Wix may list a slot's `startDate` as a LOCAL wall time with its
26
+ * `timezone` beside it ("2026-11-02T18:30:00" in America/New_York is 23:30Z); a bare `Date.parse`
27
+ * would read that in the device's zone. So a local time is placed in the slot's own zone when this
28
+ * engine has Intl and knows the zone, and read in the device's zone only when it cannot be. */
29
+ export function slotStartInstant(slot: { startDate?: string; timezone?: unknown }): number {
30
+ const start = slot.startDate ?? "";
31
+ if (!start) return NaN;
32
+ const local = LOCAL_TIME.exec(start);
33
+ if (HAS_OFFSET.test(start) || !local || typeof slot.timezone !== "string" || !slot.timezone) return Date.parse(start);
34
+ const wall = Date.UTC(+local[1], +local[2] - 1, +local[3], +local[4], +local[5], +(local[6] ?? 0));
35
+ const offset = zoneOffsetAt(wall, slot.timezone);
36
+ if (!Number.isFinite(offset)) return Date.parse(start);
37
+ // Once more at the corrected instant, so a wall time beside a DST change lands on the right side.
38
+ const again = zoneOffsetAt(wall - offset, slot.timezone);
39
+ return wall - (Number.isFinite(again) ? again : offset);
40
+ }
41
+
42
+ /** The slot a host asked to open at (`himi_time` input `start`, an ISO 8601 instant): the id of the
43
+ * slot that starts at that same instant, or "" when none does (taken since, or out of the window).
44
+ * Compared as instants, so `…T16:30:00Z` finds a slot listed as `…T18:30:00+02:00`, or as a local
45
+ * `…T18:30:00` beside `timezone: "Asia/Jerusalem"`. */
46
+ export function slotIdAt(slots: Iterable<[string, { startDate?: string; timezone?: unknown }]>, start: string): string {
47
+ const at = start ? Date.parse(start) : NaN;
48
+ if (!Number.isFinite(at)) return "";
49
+ for (const [id, slot] of slots) {
50
+ if (slotStartInstant(slot) === at) return id;
51
+ }
52
+ return "";
53
+ }
@@ -1,26 +1,65 @@
1
1
  // Host events — what a member screen tells the app hosting it about something the member did
2
- // (a booking confirmed), so a host such as Symphony can say so in its chat and refresh what it shows.
2
+ // (a booking confirmed or cancelled), so a host such as Symphony can say so in its chat and refresh
3
+ // what it shows.
3
4
  //
4
5
  // A worker dispatches `host.event` with `{ type, ...fields }` (string values only: a dispatch carries
5
6
  // one string params map). An embed (`HimalayaEmbed`) hands it to the host's `onAction` as an action
6
7
  // with that id; anywhere else nothing handles it and it is dropped. It is a notice, never a request:
7
8
  // the host owns what happens next, and the worker never waits on it or depends on its answer.
9
+ //
10
+ // The contract (hosts parse exactly this — Symphony's `PlaceHostEvent`):
11
+ // type booking.created | booking.cancelled | plan.used
12
+ // serviceName the class / service title (bookings) planName the plan's name (plan.used)
13
+ // start, end ISO 8601 instants WITH an offset (`Z` or `+02:00`); a local time is left out
14
+ // remaining sessions left on the plan, when known (a whole number)
15
+ // bookingId, serviceId, planId when known
8
16
 
9
17
  import { dispatch } from "@himalaya/dispatch";
10
18
 
11
19
  export const HOST_EVENT_ACTION_ID = "host.event";
12
20
 
13
21
  /** The events a member screen reports. */
14
- export type HostEventType = "booking.created";
22
+ export type HostEventType = "booking.created" | "booking.cancelled" | "plan.used";
15
23
 
16
- /** Tell the host about [type]. Never throws: a host that does not listen must not break the screen. */
17
- export async function emitHostEvent(type: HostEventType, fields: Record<string, string | undefined> = {}): Promise<void> {
24
+ export interface HostEventFields {
25
+ bookingId?: string;
26
+ serviceId?: string;
27
+ serviceName?: string;
28
+ planId?: string;
29
+ planName?: string;
30
+ start?: string;
31
+ end?: string;
32
+ remaining?: number | string;
33
+ }
34
+
35
+ const WITH_OFFSET = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})$/i;
36
+
37
+ /** [value] when it is an ISO 8601 instant that names its offset, else "". A slot's LOCAL wall time
38
+ * ("2026-11-02T18:30:00") would be read in the host's zone, i.e. a wrong hour, so it is left out. */
39
+ export function hostEventInstant(value: unknown): string {
40
+ return typeof value === "string" && WITH_OFFSET.test(value) && Number.isFinite(Date.parse(value)) ? value : "";
41
+ }
42
+
43
+ /** The `host.event` params for [type] and [fields]: unset and empty fields are left out, times
44
+ * without an offset are left out, and `remaining` is a whole number ≥ 0 or nothing. */
45
+ export function hostEventParams(type: HostEventType, fields: HostEventFields = {}): Record<string, string> {
18
46
  const params: Record<string, string> = { type };
19
47
  for (const [key, value] of Object.entries(fields)) {
20
- if (typeof value === "string" && value !== "") params[key] = value;
48
+ let out = "";
49
+ if (key === "start" || key === "end") out = hostEventInstant(value);
50
+ else if (key === "remaining") {
51
+ const n = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
52
+ out = Number.isInteger(n) && n >= 0 ? String(n) : "";
53
+ } else if (typeof value === "string") out = value.trim();
54
+ if (out) params[key] = out;
21
55
  }
56
+ return params;
57
+ }
58
+
59
+ /** Tell the host about [type]. Never throws: a host that does not listen must not break the screen. */
60
+ export async function emitHostEvent(type: HostEventType, fields: HostEventFields = {}): Promise<void> {
22
61
  try {
23
- await dispatch(HOST_EVENT_ACTION_ID, params);
62
+ await dispatch(HOST_EVENT_ACTION_ID, hostEventParams(type, fields));
24
63
  } catch {
25
64
  // Not embedded, or the host has no handler: nothing to tell.
26
65
  }
@@ -4,6 +4,7 @@ import { navigate } from "@himalaya/state";
4
4
  import { copyParam } from "../_shared/himi-copy.js";
5
5
  import { memberAccess, assertMemberAccess, memberAccessChanged, memberAccessDenied, type MemberAccess } from "../_shared/member-access.js";
6
6
  import { fetchMemberBookings, type MemberBookingRow } from "../_shared/member-bookings.js";
7
+ import { emitHostEvent } from "../_shared/host-events.js";
7
8
  import { dayKey, dayLabel } from "@himalaya/time";
8
9
  import { localPreferences } from "../_shared/local-preferences.js";
9
10
  import { parseWeeklyGoal, weeklyGoalProgress, weeklySessionsFromPages } from "../_shared/weekly-goal.js";
@@ -136,6 +137,9 @@ const svc = defineBackendCollection<S, MemberBookingRow>("himiBookingBookings",
136
137
  await cancelBookingAsMember(target.row.id, target.row.revision);
137
138
  await assertMemberAccess(target.member);
138
139
  cancelTarget = undefined;
140
+ // Told to the app hosting this screen (an embed's `onAction`); dropped where nothing hosts it.
141
+ await emitHostEvent("booking.cancelled", { bookingId: target.row.id, serviceId: target.row.serviceId,
142
+ serviceName: target.row.title, start: target.row.start, end: target.row.end });
139
143
  await svc.refresh();
140
144
  return derive({ ...(svc.getState() as unknown as S), actionMessage: copyParam("bookings.cancelDone", "Booking cancelled.") });
141
145
  } catch (error) {
@@ -16,7 +16,8 @@ import { formatServicePrice, serviceIsChargeable } from "../_shared/wix-service-
16
16
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
17
17
  import { upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "@himalaya/local-db";
18
18
  import { isBridgeUnavailable } from "../_shared/owner-load-error.js";
19
- import { emitHostEvent } from "../_shared/host-events.js";
19
+ import { emitHostEvent, hostEventInstant } from "../_shared/host-events.js";
20
+ import { slotIdAt } from "../_shared/himi-bookings.js";
20
21
  import { wixMediaUrl, WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
21
22
 
22
23
  // Offline-first (data-offline): booking a slot is WRITE-BEHIND against the
@@ -147,12 +148,23 @@ function inputId(): string {
147
148
  // `id` nav param (himi.open { screenId: "himi_time", id }) into worker input.
148
149
  // The screenId-slice below is #570 back-compat for pre-params releases with
149
150
  // baked `himi_time_<id>` screenIds — do not author against this.
150
- const input = (globalThis as { himalaya?: { input?: { id?: unknown; screenId?: unknown } } }).himalaya?.input;
151
+ // `serviceId` is the same thing under the name a HOST uses to open this screen (the deep-entry
152
+ // contract: Branded Lite README, "Opening at a booking").
153
+ const input = (globalThis as { himalaya?: { input?: { id?: unknown; serviceId?: unknown; screenId?: unknown } } }).himalaya?.input;
151
154
  if (typeof input?.id === "string" && input.id) return input.id;
155
+ if (typeof input?.serviceId === "string" && input.serviceId) return input.serviceId;
152
156
  const screenId = typeof input?.screenId === "string" ? input.screenId : "";
153
157
  return screenId.startsWith("himi_time_") ? screenId.slice("himi_time_".length) : "";
154
158
  }
155
159
 
160
+ /** The time a host opened this screen at (`himalaya.input.start`, ISO 8601), or "". Every load
161
+ * preselects that slot (or opens on its day) until the member picks a day or a time themselves —
162
+ * not just the first, because the boot load and the screen's own `load` can overlap. */
163
+ let pendingStart = (() => {
164
+ const start = (globalThis as { himalaya?: { input?: { start?: unknown } } }).himalaya?.input?.start;
165
+ return typeof start === "string" && Number.isFinite(Date.parse(start)) ? start : "";
166
+ })();
167
+
156
168
  function strOf(arg: unknown): string {
157
169
  if (typeof arg === "string") return arg;
158
170
  const a = arg as { value?: string; text?: string } | undefined;
@@ -534,15 +546,20 @@ const svc = defineService<S, {
534
546
  const days = daysOf(rows);
535
547
  // Keep the customer's day across a reload when it still has times;
536
548
  // otherwise open on the first day that has any.
549
+ // A host's `start` (deep entry): that slot preselected, or — taken since — its day.
550
+ const wantedStart = pendingStart;
551
+ const startRow = wantedStart ? rows.find((r) => r.id === slotIdAt(slotsById, wantedStart)) : undefined;
552
+ // The requested day even with no time left on it: "taken since" opens there, nothing selected.
553
+ const startDay = wantedStart ? days.find((d) => d.id === dayKey(wantedStart)) : undefined;
537
554
  const keep = days.find((d) => d.id === next.selectedDay && d.hasSlots);
538
- const initialDay = keep?.id ?? days.find((d) => d.hasSlots)?.id ?? days[0]?.id ?? "";
539
- const selectionAlive = slotsById.has(next.selectedSlotId);
555
+ const initialDay = startRow?.dayKey ?? startDay?.id ?? keep?.id ?? days.find((d) => d.hasSlots)?.id ?? days[0]?.id ?? "";
556
+ const selectionAlive = !!startRow || slotsById.has(next.selectedSlotId);
540
557
  const header = serviceHeaderFields(serviceRes.service);
541
558
  return derive(withDay({
542
559
  ...next,
543
560
  days,
544
- selectedSlotId: selectionAlive ? next.selectedSlotId : "",
545
- selectedLine: selectionAlive ? next.selectedLine : "",
561
+ selectedSlotId: startRow ? startRow.id : selectionAlive ? next.selectedSlotId : "",
562
+ selectedLine: startRow ? [startRow.day, startRow.title, startRow.subtitle].filter(Boolean).join(" · ") : selectionAlive ? next.selectedLine : "",
546
563
  status: "ready",
547
564
  errored: false,
548
565
  serviceName: serviceRes.service?.name || copyParam("time.serviceNameFallback", "Choose a time"),
@@ -567,6 +584,7 @@ const svc = defineService<S, {
567
584
  selectDay: (s, arg) => {
568
585
  const day = chosenDay(arg);
569
586
  if (!day || !s.days.some((d) => d.id === day)) return derive(s);
587
+ pendingStart = "";
570
588
  return derive(withDay(s, day));
571
589
  },
572
590
  refreshMember: (s, arg) => { stashCopy(arg); return probeMember(s); },
@@ -575,6 +593,7 @@ const svc = defineService<S, {
575
593
  ? arg
576
594
  : ((arg as { id?: string; slotId?: string })?.slotId ?? (arg as { id?: string })?.id ?? "");
577
595
  const selected = allRows.find((slot) => slot.id === slotId);
596
+ if (selected) pendingStart = "";
578
597
  return derive({
579
598
  ...s,
580
599
  selectedSlotId: selected ? slotId : s.selectedSlotId,
@@ -638,12 +657,14 @@ const svc = defineService<S, {
638
657
  if (!slot) return derive({ ...s, message: copyParam("time.bookSlotGone", "That time is no longer available. Try again.") });
639
658
  // A confirmed booking is reported to the app hosting this screen (an embed's `onAction`), so a
640
659
  // host chat can say so and refresh what it shows. Dropped where nothing hosts the screen.
641
- const reportBooked = (bookingId: string | undefined) => emitHostEvent("booking.created", {
642
- bookingId,
660
+ // The booking's own instants carry an offset; a slot's may be LOCAL to its timezone, which
661
+ // `emitHostEvent` leaves out rather than let the host read it in its own zone.
662
+ const reportBooked = (booking: { id?: string; startDate?: string; endDate?: string } | undefined) => emitHostEvent("booking.created", {
663
+ bookingId: booking?.id,
643
664
  serviceId: s.id,
644
665
  serviceName: s.serviceName,
645
- start: slot.startDate,
646
- end: slot.endDate,
666
+ start: hostEventInstant(booking?.startDate) || slot.startDate,
667
+ end: hostEventInstant(booking?.endDate) || slot.endDate,
647
668
  });
648
669
  // PAID service — FULLY NATIVE in-app payment (mirrors Wix's RN apps): NO
649
670
  // hosted-checkout WebView / browser redirect. The chain is
@@ -679,7 +700,7 @@ const svc = defineService<S, {
679
700
  const orderRes = hosted ? { paymentGatewayOrderId: "" } : await createOrder(checkoutId);
680
701
  const snapshotId = orderRes.paymentGatewayOrderId ?? "";
681
702
  if (!snapshotId && !hosted) {
682
- await reportBooked(bookingId);
703
+ await reportBooked(created.booking);
683
704
  return derive({ ...pending, bookingInFlight: false, successMessage: copyParam("time.successMessage", "Booked. Your confirmation is ready in My Bookings."), message: "" });
684
705
  }
685
706
  // 4. Collect payment IN-APP — wallet when this app has one wired, otherwise
@@ -701,7 +722,7 @@ const svc = defineService<S, {
701
722
  },
702
723
  });
703
724
  if (pay.kind === "paid") {
704
- await reportBooked(bookingId);
725
+ await reportBooked(created.booking);
705
726
  return derive({ ...pending, bookingInFlight: false, successMessage: copyParam("time.successMessage", "Booked. Your confirmation is ready in My Bookings."), message: "" });
706
727
  }
707
728
  if (pay.kind === "handoff") {
@@ -740,7 +761,7 @@ const svc = defineService<S, {
740
761
  // Reconcile the optimistic local id with the server-assigned booking id.
741
762
  const realId = created.booking?.id;
742
763
  if (realId && realId !== localId) { await cacheDrop(localId); await cachePut(localRow(realId)); }
743
- await reportBooked(realId);
764
+ await reportBooked(created.booking);
744
765
  return derive({
745
766
  ...pending,
746
767
  bookingInFlight: false,
@@ -444,3 +444,24 @@ export function calendarDay(date: Date, timeZone?: string): string {
444
444
  }
445
445
  return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
446
446
  }
447
+
448
+ /**
449
+ * [timeZone]'s offset from UTC in ms at instant [at] (Asia/Jerusalem in winter: 7_200_000), or NaN
450
+ * where it cannot be known — no ECMA-402 (the wrist engines) or a zone this engine does not know.
451
+ * Callers fall back to the device zone on NaN, as the formatters above do.
452
+ */
453
+ export function zoneOffsetAt(at: number, timeZone: string): number {
454
+ if (hasIntl() && Number.isFinite(at)) {
455
+ try {
456
+ const parts = new Intl.DateTimeFormat("en-US", {
457
+ timeZone, hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit",
458
+ hour: "2-digit", minute: "2-digit", second: "2-digit",
459
+ }).formatToParts(new Date(at));
460
+ const n = (type: string) => Number(parts.find((p) => p.type === type)?.value);
461
+ return Date.UTC(n("year"), n("month") - 1, n("day"), n("hour") % 24, n("minute"), n("second")) - at;
462
+ } catch {
463
+ /* a zone this engine does not know: NaN below */
464
+ }
465
+ }
466
+ return NaN;
467
+ }