@seatlayer/core 0.1.4 → 0.2.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/index.d.cts CHANGED
@@ -547,6 +547,14 @@ declare function chartBounds(doc: ChartDoc): {
547
547
  height: number;
548
548
  };
549
549
 
550
+ declare const MAX_GA_CAPACITY = 100000;
551
+ declare const MAX_EVENT_INVENTORY = 100000;
552
+ /** Stable internal inventory label for one unit of a GA area's capacity. */
553
+ declare function gaUnitLabel(areaId: string, index: number): string;
554
+ declare function gaUnitLabels(area: GAAreaObject): string[];
555
+ declare function gaAreasOf(doc: ChartDoc): GAAreaObject[];
556
+ declare function isGaUnitLabel(label: string): boolean;
557
+
550
558
  /**
551
559
  * Section membership + hide/show resolution (Batch 3.3).
552
560
  *
@@ -947,7 +955,7 @@ interface PickerSeat {
947
955
  }
948
956
  interface HoldConflict {
949
957
  label: string;
950
- reason: string;
958
+ status: string;
951
959
  }
952
960
  /** An open hold — its id, the labels it covers, and the server's expiry time (ms epoch). */
953
961
  interface HoldInfo {
@@ -956,6 +964,8 @@ interface HoldInfo {
956
964
  expiresAt: number;
957
965
  /** The held seats with their chosen ticket tier — what the host books against. */
958
966
  seats: PickerSeat[];
967
+ /** Server-authoritative commercial line items, including GA units and resolved prices. */
968
+ items?: HoldServerItem[];
959
969
  }
960
970
  /** One category's slice of a section (dot + price + count) for the summary card. */
961
971
  interface SectionCategory {
@@ -987,6 +997,21 @@ interface SectionSummary {
987
997
  interface HoldResponse {
988
998
  holdId: string;
989
999
  expiresAt: number;
1000
+ items?: HoldServerItem[];
1001
+ }
1002
+ interface HoldServerItem {
1003
+ label: string;
1004
+ objectId: string;
1005
+ objectType: 'seat' | 'booth' | 'ga';
1006
+ categoryKey: string;
1007
+ tierId: string | null;
1008
+ unitPrice: number;
1009
+ currency: string;
1010
+ quantity?: number;
1011
+ }
1012
+ interface HoldSelectionRequest {
1013
+ label: string;
1014
+ tierId?: string | null;
990
1015
  }
991
1016
  interface BestAvailableResponse extends HoldResponse {
992
1017
  labels: string[];
@@ -1002,13 +1027,14 @@ interface PickerTransport {
1002
1027
  venue?: string | null;
1003
1028
  startsAt?: number | null;
1004
1029
  currency?: string;
1030
+ mode?: string;
1005
1031
  };
1006
1032
  }>;
1007
1033
  objects(key: string): Promise<{
1008
1034
  seats: Record<string, string>;
1009
1035
  hidden?: string[];
1010
1036
  }>;
1011
- hold(key: string, labels: string[]): Promise<HoldResponse>;
1037
+ hold(key: string, selections: HoldSelectionRequest[], ttlMs?: number, replaceHoldId?: string): Promise<HoldResponse>;
1012
1038
  bestAvailable(key: string, qty: number, categoryKey?: string): Promise<BestAvailableResponse>;
1013
1039
  release(key: string, labels: string[], holdId: string): Promise<unknown>;
1014
1040
  /** Optional — the SDK omits booking (it hands the holdId to the host page). */
@@ -1079,6 +1105,8 @@ declare class PickerController {
1079
1105
  private attempt;
1080
1106
  private closed;
1081
1107
  private hold_;
1108
+ private liveStatuses;
1109
+ private currency;
1082
1110
  private expiryTimer;
1083
1111
  constructor(options: PickerOptions);
1084
1112
  get doc(): ChartDoc | null;
@@ -1098,6 +1126,8 @@ declare class PickerController {
1098
1126
  venue?: string | null;
1099
1127
  startsAt?: number | null;
1100
1128
  currency?: string;
1129
+ /** 'test' ⇒ sandbox event (hosts show a TEST MODE ribbon). */
1130
+ mode?: string;
1101
1131
  } | null>;
1102
1132
  getSelection(): PickerSeat[];
1103
1133
  clearSelection(): void;
@@ -1108,7 +1138,23 @@ declare class PickerController {
1108
1138
  * re-throws so the caller can choose the banner copy. Returns null only when
1109
1139
  * there's nothing to hold.
1110
1140
  */
1111
- hold(labelsArg?: string[]): Promise<HoldInfo | null>;
1141
+ hold(labelsArg?: string[], ttlMs?: number): Promise<HoldInfo | null>;
1142
+ /** Public GA inventory derived from the live synthetic-unit status stream. */
1143
+ getGAAreas(): {
1144
+ id: string;
1145
+ label: string;
1146
+ capacity: number;
1147
+ available: number;
1148
+ categoryKey: string;
1149
+ price: number;
1150
+ currency: string;
1151
+ tiers?: CategoryTier[];
1152
+ }[];
1153
+ /** Select a quantity from one GA area and hold it atomically. */
1154
+ holdGA(areaId: string, qty: number, options?: {
1155
+ tierId?: string | null;
1156
+ ttlMs?: number;
1157
+ }): Promise<HoldInfo | null>;
1112
1158
  /** Server-picks `qty` best free seats and holds them atomically. Throws on failure. */
1113
1159
  bestAvailable(qty: number, categoryKey?: string): Promise<HoldInfo | null>;
1114
1160
  /**
@@ -1204,8 +1250,9 @@ declare class PickerController {
1204
1250
  /** Set the open hold + (re)arm the server-authoritative expiry timer. */
1205
1251
  private setHold;
1206
1252
  private clearHold;
1207
- private onHoldExpired;
1253
+ private expireActiveHold;
1208
1254
  private applySeatsMap;
1255
+ private clearBookedHoldIfSettled;
1209
1256
  private resnapshot;
1210
1257
  private connect;
1211
1258
  private scheduleReconnect;
@@ -1283,4 +1330,4 @@ declare function formatMoney(amount: number, currency?: string, fractionDigits?:
1283
1330
  /** Bare symbol for input adornments ("€", "$", "₹"). */
1284
1331
  declare function currencySymbol(currency?: string): string;
1285
1332
 
1286
- export { ACCESSIBILITY_TYPES, type AccessibilityMeta, type AccessibilityType, type AvailabilityRule, type BoothObject, CHART_STORAGE_KEY, type Category, type CategoryTier, type ChartDoc, type ChartObject, type ChartTheme, DEFAULT_CURRENCY, type Dict, type ExpandedSeat, type Floor, type GAAreaObject, type HoldConflict, type HoldInfo, type ISeatmapRenderer, type Locale, type LodRung, type PickerCallbacks, PickerController, type PickerOptions, type PickerSeat, type PickerTransport, type Point, type RendererCallbacks, type RendererOptions, type RowObject, type RowSeatSlot, SUPPORTED_LOCALES, type SeatOverride, type SeatStatus, SeatmapRenderer, type SectionCategory, type SectionNode, type SectionObject, type SectionSummary, type SelectionLayer, type ShapeObject, type TableObject, type TextObject, UNGROUPED_ID, type ZoneDef, accessibilityMeta, allObjects, applyHidden, chartBounds, computeSections, createRenderer, currencySymbol, expandBooth, expandChart, expandRow, expandRowSlots, expandTable, floorObjects, floorsOf, formatDate, formatMoney, getLocale, hiddenObjectIds, isSectionHidden, layerOf, loadLocale, objectCenter, pointInPolygon, resolveLocale, setLocale, setMoneyLocale, setStringOverrides, stackFloors, t, tCount };
1333
+ export { ACCESSIBILITY_TYPES, type AccessibilityMeta, type AccessibilityType, type AvailabilityRule, type BoothObject, CHART_STORAGE_KEY, type Category, type CategoryTier, type ChartDoc, type ChartObject, type ChartTheme, DEFAULT_CURRENCY, type Dict, type ExpandedSeat, type Floor, type GAAreaObject, type HoldConflict, type HoldInfo, type HoldSelectionRequest, type HoldServerItem, type ISeatmapRenderer, type Locale, type LodRung, MAX_EVENT_INVENTORY, MAX_GA_CAPACITY, type PickerCallbacks, PickerController, type PickerOptions, type PickerSeat, type PickerTransport, type Point, type RendererCallbacks, type RendererOptions, type RowObject, type RowSeatSlot, SUPPORTED_LOCALES, type SeatOverride, type SeatStatus, SeatmapRenderer, type SectionCategory, type SectionNode, type SectionObject, type SectionSummary, type SelectionLayer, type ShapeObject, type TableObject, type TextObject, UNGROUPED_ID, type ZoneDef, accessibilityMeta, allObjects, applyHidden, chartBounds, computeSections, createRenderer, currencySymbol, expandBooth, expandChart, expandRow, expandRowSlots, expandTable, floorObjects, floorsOf, formatDate, formatMoney, gaAreasOf, gaUnitLabel, gaUnitLabels, getLocale, hiddenObjectIds, isGaUnitLabel, isSectionHidden, layerOf, loadLocale, objectCenter, pointInPolygon, resolveLocale, setLocale, setMoneyLocale, setStringOverrides, stackFloors, t, tCount };
package/dist/index.d.ts CHANGED
@@ -547,6 +547,14 @@ declare function chartBounds(doc: ChartDoc): {
547
547
  height: number;
548
548
  };
549
549
 
550
+ declare const MAX_GA_CAPACITY = 100000;
551
+ declare const MAX_EVENT_INVENTORY = 100000;
552
+ /** Stable internal inventory label for one unit of a GA area's capacity. */
553
+ declare function gaUnitLabel(areaId: string, index: number): string;
554
+ declare function gaUnitLabels(area: GAAreaObject): string[];
555
+ declare function gaAreasOf(doc: ChartDoc): GAAreaObject[];
556
+ declare function isGaUnitLabel(label: string): boolean;
557
+
550
558
  /**
551
559
  * Section membership + hide/show resolution (Batch 3.3).
552
560
  *
@@ -947,7 +955,7 @@ interface PickerSeat {
947
955
  }
948
956
  interface HoldConflict {
949
957
  label: string;
950
- reason: string;
958
+ status: string;
951
959
  }
952
960
  /** An open hold — its id, the labels it covers, and the server's expiry time (ms epoch). */
953
961
  interface HoldInfo {
@@ -956,6 +964,8 @@ interface HoldInfo {
956
964
  expiresAt: number;
957
965
  /** The held seats with their chosen ticket tier — what the host books against. */
958
966
  seats: PickerSeat[];
967
+ /** Server-authoritative commercial line items, including GA units and resolved prices. */
968
+ items?: HoldServerItem[];
959
969
  }
960
970
  /** One category's slice of a section (dot + price + count) for the summary card. */
961
971
  interface SectionCategory {
@@ -987,6 +997,21 @@ interface SectionSummary {
987
997
  interface HoldResponse {
988
998
  holdId: string;
989
999
  expiresAt: number;
1000
+ items?: HoldServerItem[];
1001
+ }
1002
+ interface HoldServerItem {
1003
+ label: string;
1004
+ objectId: string;
1005
+ objectType: 'seat' | 'booth' | 'ga';
1006
+ categoryKey: string;
1007
+ tierId: string | null;
1008
+ unitPrice: number;
1009
+ currency: string;
1010
+ quantity?: number;
1011
+ }
1012
+ interface HoldSelectionRequest {
1013
+ label: string;
1014
+ tierId?: string | null;
990
1015
  }
991
1016
  interface BestAvailableResponse extends HoldResponse {
992
1017
  labels: string[];
@@ -1002,13 +1027,14 @@ interface PickerTransport {
1002
1027
  venue?: string | null;
1003
1028
  startsAt?: number | null;
1004
1029
  currency?: string;
1030
+ mode?: string;
1005
1031
  };
1006
1032
  }>;
1007
1033
  objects(key: string): Promise<{
1008
1034
  seats: Record<string, string>;
1009
1035
  hidden?: string[];
1010
1036
  }>;
1011
- hold(key: string, labels: string[]): Promise<HoldResponse>;
1037
+ hold(key: string, selections: HoldSelectionRequest[], ttlMs?: number, replaceHoldId?: string): Promise<HoldResponse>;
1012
1038
  bestAvailable(key: string, qty: number, categoryKey?: string): Promise<BestAvailableResponse>;
1013
1039
  release(key: string, labels: string[], holdId: string): Promise<unknown>;
1014
1040
  /** Optional — the SDK omits booking (it hands the holdId to the host page). */
@@ -1079,6 +1105,8 @@ declare class PickerController {
1079
1105
  private attempt;
1080
1106
  private closed;
1081
1107
  private hold_;
1108
+ private liveStatuses;
1109
+ private currency;
1082
1110
  private expiryTimer;
1083
1111
  constructor(options: PickerOptions);
1084
1112
  get doc(): ChartDoc | null;
@@ -1098,6 +1126,8 @@ declare class PickerController {
1098
1126
  venue?: string | null;
1099
1127
  startsAt?: number | null;
1100
1128
  currency?: string;
1129
+ /** 'test' ⇒ sandbox event (hosts show a TEST MODE ribbon). */
1130
+ mode?: string;
1101
1131
  } | null>;
1102
1132
  getSelection(): PickerSeat[];
1103
1133
  clearSelection(): void;
@@ -1108,7 +1138,23 @@ declare class PickerController {
1108
1138
  * re-throws so the caller can choose the banner copy. Returns null only when
1109
1139
  * there's nothing to hold.
1110
1140
  */
1111
- hold(labelsArg?: string[]): Promise<HoldInfo | null>;
1141
+ hold(labelsArg?: string[], ttlMs?: number): Promise<HoldInfo | null>;
1142
+ /** Public GA inventory derived from the live synthetic-unit status stream. */
1143
+ getGAAreas(): {
1144
+ id: string;
1145
+ label: string;
1146
+ capacity: number;
1147
+ available: number;
1148
+ categoryKey: string;
1149
+ price: number;
1150
+ currency: string;
1151
+ tiers?: CategoryTier[];
1152
+ }[];
1153
+ /** Select a quantity from one GA area and hold it atomically. */
1154
+ holdGA(areaId: string, qty: number, options?: {
1155
+ tierId?: string | null;
1156
+ ttlMs?: number;
1157
+ }): Promise<HoldInfo | null>;
1112
1158
  /** Server-picks `qty` best free seats and holds them atomically. Throws on failure. */
1113
1159
  bestAvailable(qty: number, categoryKey?: string): Promise<HoldInfo | null>;
1114
1160
  /**
@@ -1204,8 +1250,9 @@ declare class PickerController {
1204
1250
  /** Set the open hold + (re)arm the server-authoritative expiry timer. */
1205
1251
  private setHold;
1206
1252
  private clearHold;
1207
- private onHoldExpired;
1253
+ private expireActiveHold;
1208
1254
  private applySeatsMap;
1255
+ private clearBookedHoldIfSettled;
1209
1256
  private resnapshot;
1210
1257
  private connect;
1211
1258
  private scheduleReconnect;
@@ -1283,4 +1330,4 @@ declare function formatMoney(amount: number, currency?: string, fractionDigits?:
1283
1330
  /** Bare symbol for input adornments ("€", "$", "₹"). */
1284
1331
  declare function currencySymbol(currency?: string): string;
1285
1332
 
1286
- export { ACCESSIBILITY_TYPES, type AccessibilityMeta, type AccessibilityType, type AvailabilityRule, type BoothObject, CHART_STORAGE_KEY, type Category, type CategoryTier, type ChartDoc, type ChartObject, type ChartTheme, DEFAULT_CURRENCY, type Dict, type ExpandedSeat, type Floor, type GAAreaObject, type HoldConflict, type HoldInfo, type ISeatmapRenderer, type Locale, type LodRung, type PickerCallbacks, PickerController, type PickerOptions, type PickerSeat, type PickerTransport, type Point, type RendererCallbacks, type RendererOptions, type RowObject, type RowSeatSlot, SUPPORTED_LOCALES, type SeatOverride, type SeatStatus, SeatmapRenderer, type SectionCategory, type SectionNode, type SectionObject, type SectionSummary, type SelectionLayer, type ShapeObject, type TableObject, type TextObject, UNGROUPED_ID, type ZoneDef, accessibilityMeta, allObjects, applyHidden, chartBounds, computeSections, createRenderer, currencySymbol, expandBooth, expandChart, expandRow, expandRowSlots, expandTable, floorObjects, floorsOf, formatDate, formatMoney, getLocale, hiddenObjectIds, isSectionHidden, layerOf, loadLocale, objectCenter, pointInPolygon, resolveLocale, setLocale, setMoneyLocale, setStringOverrides, stackFloors, t, tCount };
1333
+ export { ACCESSIBILITY_TYPES, type AccessibilityMeta, type AccessibilityType, type AvailabilityRule, type BoothObject, CHART_STORAGE_KEY, type Category, type CategoryTier, type ChartDoc, type ChartObject, type ChartTheme, DEFAULT_CURRENCY, type Dict, type ExpandedSeat, type Floor, type GAAreaObject, type HoldConflict, type HoldInfo, type HoldSelectionRequest, type HoldServerItem, type ISeatmapRenderer, type Locale, type LodRung, MAX_EVENT_INVENTORY, MAX_GA_CAPACITY, type PickerCallbacks, PickerController, type PickerOptions, type PickerSeat, type PickerTransport, type Point, type RendererCallbacks, type RendererOptions, type RowObject, type RowSeatSlot, SUPPORTED_LOCALES, type SeatOverride, type SeatStatus, SeatmapRenderer, type SectionCategory, type SectionNode, type SectionObject, type SectionSummary, type SelectionLayer, type ShapeObject, type TableObject, type TextObject, UNGROUPED_ID, type ZoneDef, accessibilityMeta, allObjects, applyHidden, chartBounds, computeSections, createRenderer, currencySymbol, expandBooth, expandChart, expandRow, expandRowSlots, expandTable, floorObjects, floorsOf, formatDate, formatMoney, gaAreasOf, gaUnitLabel, gaUnitLabels, getLocale, hiddenObjectIds, isGaUnitLabel, isSectionHidden, layerOf, loadLocale, objectCenter, pointInPolygon, resolveLocale, setLocale, setMoneyLocale, setStringOverrides, stackFloors, t, tCount };
package/dist/index.js CHANGED
@@ -364,16 +364,35 @@ function chartBounds(doc) {
364
364
  };
365
365
  }
366
366
 
367
+ // src/core/ga.ts
368
+ var PREFIX = "__sl_ga__";
369
+ var MAX_GA_CAPACITY = 1e5;
370
+ var MAX_EVENT_INVENTORY = 1e5;
371
+ function gaUnitLabel(areaId, index) {
372
+ return `${PREFIX}${encodeURIComponent(areaId)}__${index + 1}`;
373
+ }
374
+ function gaUnitLabels(area) {
375
+ const capacity = Math.min(MAX_GA_CAPACITY, Math.max(0, Math.floor(area.capacity)));
376
+ return Array.from({ length: capacity }, (_, i) => gaUnitLabel(area.id, i));
377
+ }
378
+ function gaAreasOf(doc) {
379
+ return allObjects(doc).filter((o) => o.type === "gaArea");
380
+ }
381
+ function isGaUnitLabel(label) {
382
+ return label.startsWith(PREFIX);
383
+ }
384
+
367
385
  // src/core/sections.ts
368
386
  var UNGROUPED_ID = "__ungrouped__";
369
387
  function objectSeatLabels(o) {
370
388
  if (o.type === "row") return expandRow(o).map((s) => s.label);
371
389
  if (o.type === "table") return expandTable(o).map((s) => s.label);
372
390
  if (o.type === "booth") return expandBooth(o).map((s) => s.label);
391
+ if (o.type === "gaArea") return gaUnitLabels(o);
373
392
  return [];
374
393
  }
375
394
  function isSeatObject(o) {
376
- return o.type === "row" || o.type === "table" || o.type === "booth";
395
+ return o.type === "row" || o.type === "table" || o.type === "booth" || o.type === "gaArea";
377
396
  }
378
397
  function computeSections(doc) {
379
398
  const objs = allObjects(doc);
@@ -501,6 +520,7 @@ var en = {
501
520
  "picker.holdExpired": "Your hold expired \u2014 the seats were released. Pick again.",
502
521
  "picker.seatTaken": "Seat {label} was just taken by another buyer.",
503
522
  "picker.poweredBy": "Powered by SeatLayer",
523
+ "picker.testMode": "TEST MODE",
504
524
  "picker.colorblind": "Colorblind-friendly colors",
505
525
  "picker.orphanHint": "This leaves a single seat stranded \u2014 consider shifting one seat over.",
506
526
  "picker.seats.one": "{count} seat",
@@ -2575,6 +2595,8 @@ var PickerController = class {
2575
2595
  this.closed = false;
2576
2596
  // hold state
2577
2597
  this.hold_ = null;
2598
+ this.liveStatuses = /* @__PURE__ */ new Map();
2599
+ this.currency = "USD";
2578
2600
  this.expiryTimer = null;
2579
2601
  /** Whether the last emitted hint was non-null (avoids clearing repeatedly). */
2580
2602
  this.hintShown = false;
@@ -2634,6 +2656,7 @@ var PickerController = class {
2634
2656
  this.allIds.push(s.id);
2635
2657
  }
2636
2658
  const currency = res.event.currency ?? this.opts.currency;
2659
+ this.currency = currency ?? "USD";
2637
2660
  const renderer = createRenderer(host, {
2638
2661
  maxSelection: this.maxSelection,
2639
2662
  confirmSelection: this.opts.confirmSelection,
@@ -2682,7 +2705,9 @@ var PickerController = class {
2682
2705
  eventName: res.event.name,
2683
2706
  venue: res.event.venue,
2684
2707
  startsAt: res.event.startsAt,
2685
- currency
2708
+ currency,
2709
+ // 'test' ⇒ sandbox event: hosts render a TEST MODE ribbon (Phase 11).
2710
+ mode: res.event.mode ?? "live"
2686
2711
  };
2687
2712
  }
2688
2713
  // ---- selection ------------------------------------------------------------
@@ -2705,21 +2730,72 @@ var PickerController = class {
2705
2730
  * re-throws so the caller can choose the banner copy. Returns null only when
2706
2731
  * there's nothing to hold.
2707
2732
  */
2708
- async hold(labelsArg) {
2733
+ async hold(labelsArg, ttlMs) {
2709
2734
  const r = this.renderer;
2710
2735
  if (!r) return null;
2711
2736
  const labels = labelsArg ?? r.getSelection().map((s) => s.label);
2712
- if (!labels.length) return null;
2713
- if (this.hold_ && this.holdCovers(labels)) return this.hold_;
2737
+ const existingGA = (this.hold_?.items ?? []).filter((item) => item.objectType === "ga");
2738
+ const combinedLabels = [.../* @__PURE__ */ new Set([...existingGA.map((item) => item.label), ...labels])];
2739
+ if (!combinedLabels.length) return null;
2740
+ if (this.hold_ && this.holdCovers(combinedLabels)) return this.hold_;
2714
2741
  try {
2715
- const result = await this.api.hold(this.key, labels);
2716
- this.setHold({ holdId: result.holdId, labels: [...labels], expiresAt: result.expiresAt });
2742
+ const selections = combinedLabels.map((label) => {
2743
+ const ga = existingGA.find((item) => item.label === label);
2744
+ if (ga) return { label, tierId: ga.tierId };
2745
+ const seat = this.labelToSeat.get(label);
2746
+ const resolved = seat ? this.toSeat(seat) : null;
2747
+ return { label, ...resolved?.tierId ? { tierId: resolved.tierId } : {} };
2748
+ });
2749
+ const result = await this.api.hold(this.key, selections, ttlMs, this.hold_?.holdId);
2750
+ this.setHold({ holdId: result.holdId, labels: combinedLabels, expiresAt: result.expiresAt, items: result.items });
2717
2751
  return this.hold_;
2718
2752
  } catch (err) {
2719
2753
  this.handle409Conflicts(err);
2720
2754
  throw err;
2721
2755
  }
2722
2756
  }
2757
+ /** Public GA inventory derived from the live synthetic-unit status stream. */
2758
+ getGAAreas() {
2759
+ const doc = this.visibleDoc();
2760
+ if (!doc) return [];
2761
+ return gaAreasOf(doc).map((area) => {
2762
+ const category = doc.categories.find((candidate) => candidate.key === area.categoryKey);
2763
+ return {
2764
+ id: area.id,
2765
+ label: area.label,
2766
+ capacity: Math.max(0, Math.floor(area.capacity)),
2767
+ available: gaUnitLabels(area).filter((label) => (this.liveStatuses.get(label) ?? "free") === "free").length,
2768
+ categoryKey: area.categoryKey,
2769
+ tiers: category?.tiers,
2770
+ price: category?.tiers?.[0]?.price ?? category?.price ?? 0,
2771
+ currency: this.currency
2772
+ };
2773
+ });
2774
+ }
2775
+ /** Select a quantity from one GA area and hold it atomically. */
2776
+ async holdGA(areaId, qty, options = {}) {
2777
+ const doc = this.visibleDoc();
2778
+ if (!doc || !Number.isFinite(qty) || qty < 1) return null;
2779
+ const area = gaAreasOf(doc).find((candidate) => candidate.id === areaId);
2780
+ if (!area) return null;
2781
+ const labels = gaUnitLabels(area).filter((label) => !this.hold_?.labels.includes(label) && (this.liveStatuses.get(label) ?? "free") === "free").slice(0, Math.floor(qty));
2782
+ if (labels.length !== Math.floor(qty)) return null;
2783
+ const selections = /* @__PURE__ */ new Map();
2784
+ for (const item of this.hold_?.items ?? []) selections.set(item.label, { label: item.label, tierId: item.tierId });
2785
+ if (this.hold_ && !this.hold_?.items?.length) {
2786
+ for (const seat of this.hold_.seats) selections.set(seat.label, { label: seat.label, ...seat.tierId ? { tierId: seat.tierId } : {} });
2787
+ }
2788
+ for (const seat of this.renderer?.getSelection() ?? []) {
2789
+ const resolved = this.labelToSeat.get(seat.label);
2790
+ const chosen = resolved ? this.toSeat(resolved) : null;
2791
+ selections.set(seat.label, { label: seat.label, ...chosen?.tierId ? { tierId: chosen.tierId } : {} });
2792
+ }
2793
+ for (const label of labels) selections.set(label, { label, ...options.tierId ? { tierId: options.tierId } : {} });
2794
+ const combined = [...selections.values()];
2795
+ const result = await this.api.hold(this.key, combined, options.ttlMs, this.hold_?.holdId);
2796
+ this.setHold({ holdId: result.holdId, labels: combined.map((s) => s.label), expiresAt: result.expiresAt, items: result.items });
2797
+ return this.hold_;
2798
+ }
2723
2799
  /** Server-picks `qty` best free seats and holds them atomically. Throws on failure. */
2724
2800
  async bestAvailable(qty, categoryKey) {
2725
2801
  const r = this.renderer;
@@ -2730,7 +2806,7 @@ var PickerController = class {
2730
2806
  r.clearSelection();
2731
2807
  const ids = result.labels.map((l) => this.labelToId.get(l)).filter((v) => !!v);
2732
2808
  if (ids.length) r.setStatus(ids, "held");
2733
- this.setHold({ holdId: result.holdId, labels: [...result.labels], expiresAt: result.expiresAt });
2809
+ this.setHold({ holdId: result.holdId, labels: [...result.labels], expiresAt: result.expiresAt, items: result.items });
2734
2810
  const seats = result.labels.map((l) => this.labelToSeat.get(l)).filter((s) => !!s).map((s) => this.toSeat(s));
2735
2811
  this.opts.onSelectionChange?.(seats);
2736
2812
  return this.hold_;
@@ -2759,9 +2835,19 @@ var PickerController = class {
2759
2835
  if (this.hold_ && this.holdCovers(labels)) {
2760
2836
  holdId = this.hold_.holdId;
2761
2837
  } else {
2762
- const h = await this.api.hold(this.key, labels);
2838
+ const replaceHoldId = this.hold_?.holdId;
2839
+ const h = await this.api.hold(
2840
+ this.key,
2841
+ labels.map((label) => {
2842
+ const seat = this.labelToSeat.get(label);
2843
+ const resolved = seat ? this.toSeat(seat) : null;
2844
+ return { label, ...resolved?.tierId ? { tierId: resolved.tierId } : {} };
2845
+ }),
2846
+ void 0,
2847
+ replaceHoldId
2848
+ );
2763
2849
  holdId = h.holdId;
2764
- this.setHold({ holdId, labels: [...labels], expiresAt: h.expiresAt });
2850
+ this.setHold({ holdId, labels: [...labels], expiresAt: h.expiresAt, items: h.items });
2765
2851
  }
2766
2852
  await this.api.book(this.key, labels, holdId, bookingRef);
2767
2853
  } catch (err) {
@@ -3077,7 +3163,13 @@ var PickerController = class {
3077
3163
  }
3078
3164
  holdCovers(labels) {
3079
3165
  const h = this.hold_;
3080
- return !!h && h.labels.length === labels.length && labels.every((l) => h.labels.includes(l));
3166
+ if (!h || h.labels.length !== labels.length || !labels.every((l) => h.labels.includes(l))) return false;
3167
+ const tiers = new Map((h.items ?? []).map((item) => [item.label, item.tierId]));
3168
+ return labels.every((label) => {
3169
+ const seat = this.labelToSeat.get(label);
3170
+ const currentTier = seat ? this.toSeat(seat).tierId ?? null : null;
3171
+ return !tiers.has(label) || tiers.get(label) === currentTier;
3172
+ });
3081
3173
  }
3082
3174
  handle409Conflicts(err) {
3083
3175
  const { status, conflicts } = errInfo(err);
@@ -3097,7 +3189,7 @@ var PickerController = class {
3097
3189
  this.hold_ = full;
3098
3190
  if (this.expiryTimer) clearTimeout(this.expiryTimer);
3099
3191
  const ms = Math.max(0, full.expiresAt - Date.now());
3100
- this.expiryTimer = setTimeout(() => this.onHoldExpired(), ms);
3192
+ this.expiryTimer = setTimeout(() => void this.expireActiveHold(), ms);
3101
3193
  this.opts.onHold?.(full);
3102
3194
  }
3103
3195
  clearHold() {
@@ -3107,18 +3199,31 @@ var PickerController = class {
3107
3199
  this.expiryTimer = null;
3108
3200
  }
3109
3201
  }
3110
- onHoldExpired() {
3202
+ async expireActiveHold() {
3111
3203
  const hold = this.hold_;
3112
- this.clearHold();
3113
- if (hold) {
3114
- const ids = hold.labels.map((l) => this.labelToId.get(l)).filter((v) => !!v);
3115
- if (ids.length) this.renderer?.setStatus(ids, "free");
3204
+ if (!hold) return;
3205
+ try {
3206
+ const snap = await this.api.objects(this.key);
3207
+ if (this.hold_?.holdId !== hold.holdId) return;
3208
+ this.applySeatsMap(snap.seats);
3209
+ } catch {
3210
+ if (this.hold_?.holdId === hold.holdId) {
3211
+ this.expiryTimer = setTimeout(() => void this.expireActiveHold(), 2e3);
3212
+ }
3213
+ return;
3214
+ }
3215
+ if (this.hold_?.holdId !== hold.holdId) return;
3216
+ if (hold.labels.some((label) => this.liveStatuses.get(label) === "held")) {
3217
+ this.expiryTimer = setTimeout(() => void this.expireActiveHold(), 1e3);
3218
+ return;
3116
3219
  }
3220
+ this.clearHold();
3117
3221
  this.opts.onHoldExpired?.();
3118
3222
  }
3119
3223
  applySeatsMap(seats) {
3120
3224
  const r = this.renderer;
3121
3225
  if (!r) return;
3226
+ this.liveStatuses = new Map(Object.entries(seats));
3122
3227
  if (this.allIds.length) r.setStatus(this.allIds, "free");
3123
3228
  const byStatus = { free: [], held: [], booked: [], not_for_sale: [] };
3124
3229
  for (const [label, st] of Object.entries(seats)) {
@@ -3129,6 +3234,10 @@ var PickerController = class {
3129
3234
  if (byStatus[st].length) r.setStatus(byStatus[st], st);
3130
3235
  });
3131
3236
  this.opts.onStatusChange?.();
3237
+ this.clearBookedHoldIfSettled();
3238
+ }
3239
+ clearBookedHoldIfSettled() {
3240
+ if (this.hold_?.labels.every((label) => this.liveStatuses.get(label) === "booked")) this.clearHold();
3132
3241
  }
3133
3242
  async resnapshot() {
3134
3243
  try {
@@ -3171,6 +3280,7 @@ var PickerController = class {
3171
3280
  this.applySeatsMap(m.seats);
3172
3281
  } else if (Array.isArray(m.changes)) {
3173
3282
  for (const ch of m.changes) {
3283
+ this.liveStatuses.set(ch.label, ch.status);
3174
3284
  const id = this.labelToId.get(ch.label);
3175
3285
  if (!id) continue;
3176
3286
  const next = mapStatus(ch.status);
@@ -3179,6 +3289,7 @@ var PickerController = class {
3179
3289
  }
3180
3290
  r.setStatus([id], next);
3181
3291
  }
3292
+ this.clearBookedHoldIfSettled();
3182
3293
  this.opts.onStatusChange?.();
3183
3294
  }
3184
3295
  };
@@ -3206,9 +3317,9 @@ var PickerController = class {
3206
3317
 
3207
3318
  // src/i18n/bundles.ts
3208
3319
  var LOADERS = {
3209
- es: () => import("./es-YNYMMP37.js").then((m) => ({ default: m.es })),
3210
- de: () => import("./de-A4KJQ374.js").then((m) => ({ default: m.de })),
3211
- fr: () => import("./fr-DQ4KWKKF.js").then((m) => ({ default: m.fr }))
3320
+ es: () => import("./es-MLM3IFIY.js").then((m) => ({ default: m.es })),
3321
+ de: () => import("./de-YMAVF5S2.js").then((m) => ({ default: m.de })),
3322
+ fr: () => import("./fr-N7AAVATM.js").then((m) => ({ default: m.fr }))
3212
3323
  };
3213
3324
  var loaded = /* @__PURE__ */ new Set(["en"]);
3214
3325
  async function loadLocale(code) {
@@ -3231,6 +3342,8 @@ export {
3231
3342
  ACCESSIBILITY_TYPES,
3232
3343
  CHART_STORAGE_KEY,
3233
3344
  DEFAULT_CURRENCY,
3345
+ MAX_EVENT_INVENTORY,
3346
+ MAX_GA_CAPACITY,
3234
3347
  PickerController,
3235
3348
  SUPPORTED_LOCALES,
3236
3349
  SeatmapRenderer,
@@ -3251,8 +3364,12 @@ export {
3251
3364
  floorsOf,
3252
3365
  formatDate,
3253
3366
  formatMoney,
3367
+ gaAreasOf,
3368
+ gaUnitLabel,
3369
+ gaUnitLabels,
3254
3370
  getLocale,
3255
3371
  hiddenObjectIds,
3372
+ isGaUnitLabel,
3256
3373
  isSectionHidden,
3257
3374
  layerOf,
3258
3375
  loadLocale,