@curless/hotel-storefront 0.49.0 → 0.49.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/huazhu/backend.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/huazhu/backend.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { createHash, randomBytes, randomUUID } from 'node:crypto';
1
+ import { randomBytes, randomUUID } from 'node:crypto';
2
2
  import { DEFAULT_PAGE, MAX_PAGE, REQUEST_KINDS, REQUEST_NOTE_MAX, guestTooLong, partyNeedsOf, } from '../backend.js';
3
3
  import { addDays, arrivalInstant, atHotelTime, nights as nightsBetween, stayProblem, stayProblemOf, todayAt, } from '../dates.js';
4
4
  import { byWords, splitByStay } from '../demo-hotels.js';
@@ -8,6 +8,7 @@ import { BOOKING_LIMITS, DAY_MS, HOUR_MS, UNPAID_PER_STRICT, WindowCounter, } fr
8
8
  import { formatMoney, minorFactor, money, multiplyMoney, toMinor } from '../money.js';
9
9
  import { UnionPayError, expiryMMyy, lastFour, } from '../payments/unionpay.js';
10
10
  import { bedKindsOf } from '../room-filters.js';
11
+ import { SAME_BOOKING_MS, guestDigest, singleFlight } from '../same-booking.js';
11
12
  import { MAX_ROOMS_PER_LINE, quoteLineKey } from '../trip.js';
12
13
  import { IN_REQUEST_AT_MS, NOT_FOUND, StoreWriteError, nextCheckAt, statusOf, } from './bookings.js';
13
14
  import { describePolicy, feeFor, freeUntilOf, nightAuditOf, parseCancelPolicy, penaltyAt, phasesOf, refundableFrom, stepsOf, } from './cancel-policy.js';
@@ -62,8 +63,6 @@ export const newPublicCode = (taken) => {
62
63
  return code;
63
64
  }
64
65
  };
65
- /** The same booking asked for again this soon - same conversation, email, stays and guest (`tripKey`) - is answered with the first. */
66
- const SAME_TRIP_MS = 10 * 60_000;
67
66
  /**
68
67
  * The most hotels priced for one search. A city can hold hundreds of Huazhu
69
68
  * hotels, and every one priced is two calls to Huazhu; past this, the page is
@@ -563,43 +562,23 @@ export const createHuazhuHotels = (opts) => {
563
562
  const unpaidAsk = (b) => !!b.prepaid &&
564
563
  !paidInAdvance(b.prepaid) &&
565
564
  (b.status === 'awaiting-payment' || b.status === 'expired');
566
- /** Requests in flight, by conversation and by email - see `single`. */
567
- const flights = new Map();
565
+ /** Every booking request asked and not yet answered - what `drain` waits for. */
566
+ const asking = new Set();
568
567
  /** Set by `drain`: a stopping process starts no booking. */
569
568
  let closing = false;
570
569
  /**
571
- * One booking request at a time per conversation and per email.
572
- *
573
- * ⚠️ TWO AT ONCE BOOKED TWICE. The trip is emptied only when a request
574
- * succeeds, and every request has its own distributorResId: a guest who
575
- * pressed twice, or a host that retried, had two real bookings (2026-10-05
576
- * audit, P1-2). The second waits for the first, then finds its trip
577
- * already booked.
570
+ * One booking request at a time per conversation and per email - the shared
571
+ * `singleFlight` (`../same-booking.js`), whose reasons it keeps (2026-10-05
572
+ * audit, P1-2: two at once booked twice). Every request asked through it is
573
+ * also remembered in `asking` until it is answered, for `drain`: the shared
574
+ * helper keeps its locks to itself.
578
575
  */
579
- const single = async (keys, run) => {
580
- for (;;) {
581
- const busy = keys.flatMap((k) => {
582
- const p = flights.get(k);
583
- return p ? [p] : [];
584
- });
585
- if (busy.length === 0)
586
- break;
587
- await Promise.allSettled(busy);
588
- }
589
- // Nothing awaited between the check above and these lines: no other
590
- // request can slip in.
591
- const p = run();
592
- const done = p.then(() => undefined, () => undefined);
593
- for (const k of keys)
594
- flights.set(k, done);
595
- try {
596
- return await p;
597
- }
598
- finally {
599
- for (const k of keys)
600
- if (flights.get(k) === done)
601
- flights.delete(k);
602
- }
576
+ const sameBooking = singleFlight();
577
+ const single = (keys, run) => {
578
+ const asked = sameBooking(keys, run);
579
+ asking.add(asked);
580
+ void asked.finally(() => asking.delete(asked)).catch(() => { });
581
+ return asked;
603
582
  };
604
583
  // ------------------------------------------------------------- lookups ----
605
584
  /**
@@ -1728,8 +1707,6 @@ export const createHuazhuHotels = (opts) => {
1728
1707
  * matched by.
1729
1708
  */
1730
1709
  const conversationOf = (who) => who.tripOwner ?? who.sessionId;
1731
- /** A name for a booking in what the guest is told. */
1732
- const named = (b) => `${b.hotelName} (${b.confirmationCode})`;
1733
1710
  /**
1734
1711
  * A booking that still counts against the limits on bookings still to come
1735
1712
  * (`perEmailLive`, `perConversationLive`) - and the one set the guest is told
@@ -1778,7 +1755,7 @@ export const createHuazhuHotels = (opts) => {
1778
1755
  .filter((b) => b.tripKey === tripKey &&
1779
1756
  b.owners.includes(conversationOf(who)) &&
1780
1757
  b.guest.email.trim().toLowerCase() === email &&
1781
- now() - Date.parse(b.createdAt) < SAME_TRIP_MS &&
1758
+ now() - Date.parse(b.createdAt) < SAME_BOOKING_MS &&
1782
1759
  isLive(b));
1783
1760
  const ref = earlier[0]?.tripRef;
1784
1761
  const before = ref ? store.intent(ref) : undefined;
@@ -3549,7 +3526,7 @@ export const createHuazhuHotels = (opts) => {
3549
3526
  closing = true;
3550
3527
  // The steps on payments too - a refund or a booking half taken when the
3551
3528
  // process stopped was lost (audit P1-3, P0-B).
3552
- await Promise.allSettled([...new Set(flights.values()), ...steps]);
3529
+ await Promise.allSettled([...asking, ...steps]);
3553
3530
  },
3554
3531
  async searchHotels(q) {
3555
3532
  const terms = (q.query ?? '')
@@ -4082,17 +4059,10 @@ export const createHuazhuHotels = (opts) => {
4082
4059
  // says it again in other words - 「需要安静的房间」 then 「希望房间安静」 -
4083
4060
  // which booked a second room, or opened a second payment. The requests
4084
4061
  // are a closed list and the arrival a time; the name and the phone are
4085
- // the same person written two ways (full-width, spacing) made one.
4086
- const same = (t) => t.normalize('NFKC').trim().replace(/\s+/g, ' ').toLowerCase();
4087
- const whoFor = createHash('sha256')
4088
- .update(JSON.stringify([
4089
- same(guest.name),
4090
- (guest.phone ?? '').normalize('NFKC').replace(/\D/g, ''),
4091
- [...(requests?.kinds ?? [])].sort(),
4092
- requests?.arrivingAt ?? '',
4093
- ]))
4094
- .digest('hex')
4095
- .slice(0, 16);
4062
+ // the same person written two ways (full-width, spacing) made one. The
4063
+ // digest is the shared `guestDigest` (`../same-booking.js`, also the
4064
+ // demonstration backend's); the key around it stays this backend's.
4065
+ const whoFor = guestDigest(guest, requests);
4096
4066
  const tripKey = `${lines
4097
4067
  .map((l) => l.kind === 'room'
4098
4068
  ? `${l.hotelId}|${l.roomId}|${l.stay.checkIn}|${l.stay.checkOut}|${l.stay.guests}|${l.rooms ?? 1}|${l.hourly?.start ?? ''}`