@seatlayer/js 0.36.0 → 0.36.2

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.cjs CHANGED
@@ -59,6 +59,8 @@ __export(index_exports, {
59
59
  createBuyerAccessContext: () => createBuyerAccessContext,
60
60
  createControllerSink: () => createControllerSink,
61
61
  dropReviewRows: () => dropReviewRows,
62
+ isPublicChannelId: () => isPublicChannelId,
63
+ markerLetter: () => markerLetter,
62
64
  markerOf: () => markerOf,
63
65
  mutationCount: () => mutationCount,
64
66
  needsMoveConfirmation: () => needsMoveConfirmation,
@@ -5989,8 +5991,11 @@ function attachPickerFrame(iframe, opts = {}) {
5989
5991
  var import_core3 = require("@seatlayer/core");
5990
5992
 
5991
5993
  // src/channelPlan.ts
5992
- var PUBLIC_CHANNEL_ID = "";
5994
+ var PUBLIC_CHANNEL_ID = "public";
5993
5995
  var PUBLIC_CHANNEL_NAME = "Public sale";
5996
+ function isPublicChannelId(id) {
5997
+ return id == null || id === "" || id === PUBLIC_CHANNEL_ID;
5998
+ }
5994
5999
  var CHANNEL_COLORS = [
5995
6000
  "#a78bfa",
5996
6001
  "#2dd4bf",
@@ -6005,27 +6010,35 @@ var CHANNEL_COLORS = [
6005
6010
  ];
6006
6011
  var PUBLIC_CHANNEL_COLOR = "#f4b740";
6007
6012
  var LETTERS = "ABCDEFGHJKLMNPQRSTUVWXYZ";
6013
+ function markerLetter(raw, fallback) {
6014
+ const text = (raw ?? "").trim();
6015
+ const letter = /\p{L}/u.exec(text)?.[0] ?? text[0] ?? "";
6016
+ return (letter || fallback).toUpperCase().slice(0, 1);
6017
+ }
6008
6018
  function suggestMarker(name, taken) {
6009
- const used = new Set([...taken].map((m) => m.trim().toUpperCase()).filter(Boolean));
6010
- const first = (name.trim()[0] ?? "").toUpperCase();
6019
+ const used = new Set([...taken].map((m) => markerLetter(m, "")).filter(Boolean));
6020
+ const first = markerLetter(name, "");
6011
6021
  const letter = LETTERS.includes(first) && !used.has(first) ? first : [...LETTERS].find((candidate) => !used.has(candidate)) ?? (first || "X");
6012
6022
  return { letter, color: CHANNEL_COLORS[used.size % CHANNEL_COLORS.length] };
6013
6023
  }
6014
6024
  function markerOf(channel, index = 0) {
6015
- if (channel.id === PUBLIC_CHANNEL_ID) {
6016
- return { letter: (channel.marker || "P").slice(0, 2).toUpperCase(), color: channel.color || PUBLIC_CHANNEL_COLOR };
6025
+ if (isPublicChannelId(channel.id)) {
6026
+ return {
6027
+ letter: markerLetter(channel.marker, "P"),
6028
+ color: channel.color || PUBLIC_CHANNEL_COLOR
6029
+ };
6017
6030
  }
6018
- const letter = (channel.marker || channel.name.trim()[0] || "?").slice(0, 2).toUpperCase();
6031
+ const letter = markerLetter(channel.marker || channel.name, "?");
6019
6032
  return { letter, color: channel.color || CHANNEL_COLORS[index % CHANNEL_COLORS.length] };
6020
6033
  }
6021
6034
  function selectionSources(labels, allocation, list) {
6022
6035
  const counts = /* @__PURE__ */ new Map();
6023
6036
  for (const label of labels) {
6024
- const channelId = allocation.get(label) ?? PUBLIC_CHANNEL_ID;
6037
+ const channelId = normalizeChannelId(allocation.get(label));
6025
6038
  counts.set(channelId, (counts.get(channelId) ?? 0) + 1);
6026
6039
  }
6027
6040
  const order = [
6028
- { id: PUBLIC_CHANNEL_ID, name: list?.publicSale.name ?? PUBLIC_CHANNEL_NAME },
6041
+ { id: PUBLIC_CHANNEL_ID, name: list?.publicSale?.name ?? PUBLIC_CHANNEL_NAME },
6029
6042
  ...(list?.channels ?? []).map((channel) => ({ id: channel.id, name: channel.name }))
6030
6043
  ];
6031
6044
  const rows = [];
@@ -6035,10 +6048,17 @@ function selectionSources(labels, allocation, list) {
6035
6048
  counts.delete(entry.id);
6036
6049
  }
6037
6050
  for (const [channelId, count] of counts) {
6038
- rows.push({ channelId, name: channelId ? "Another channel" : PUBLIC_CHANNEL_NAME, count });
6051
+ rows.push({
6052
+ channelId,
6053
+ name: isPublicChannelId(channelId) ? PUBLIC_CHANNEL_NAME : "Another channel",
6054
+ count
6055
+ });
6039
6056
  }
6040
6057
  return rows;
6041
6058
  }
6059
+ function normalizeChannelId(id) {
6060
+ return isPublicChannelId(id) ? PUBLIC_CHANNEL_ID : id;
6061
+ }
6042
6062
  var SKIP_SAMPLE = 12;
6043
6063
  function skipBucket(labels) {
6044
6064
  return {
@@ -6048,7 +6068,8 @@ function skipBucket(labels) {
6048
6068
  };
6049
6069
  }
6050
6070
  function planAssignment(input) {
6051
- const { labels, targetChannelId, allocation, statusOf, nameOf } = input;
6071
+ const { labels, allocation, statusOf, nameOf } = input;
6072
+ const targetChannelId = normalizeChannelId(input.targetChannelId);
6052
6073
  const seen = /* @__PURE__ */ new Set();
6053
6074
  let fromPublic = 0;
6054
6075
  let alreadyIn = 0;
@@ -6064,7 +6085,7 @@ function planAssignment(input) {
6064
6085
  missing.push(label);
6065
6086
  continue;
6066
6087
  }
6067
- const current = allocation.get(label) ?? PUBLIC_CHANNEL_ID;
6088
+ const current = normalizeChannelId(allocation.get(label));
6068
6089
  if (current === targetChannelId) {
6069
6090
  alreadyIn += 1;
6070
6091
  continue;
@@ -6247,12 +6268,38 @@ var ManageApi = class {
6247
6268
  pub(path) {
6248
6269
  return fetch(`${this.base}${path}`, { credentials: "omit" }).then((r) => parse(r));
6249
6270
  }
6250
- // ---- realtime read (public, no token) ----
6271
+ // ---- realtime read ----
6272
+ /** The chart geometry. Genuinely public — it is the same map buyers see. */
6251
6273
  chart(key) {
6252
6274
  return this.pub(`/pub/events/${encodeURIComponent(key)}/chart`);
6253
6275
  }
6276
+ /**
6277
+ * The ORGANIZER's seat map: physical state, token-authed.
6278
+ *
6279
+ * This used to read `/pub/events/:key/objects` with no credential, which
6280
+ * answers with the BUYER projection — every unit the caller may not buy
6281
+ * collapses to a neutral `blocked`. An anonymous caller may buy only Public
6282
+ * sale inventory, so the cockpit rendered every channel-allocated seat as
6283
+ * blocked and then computed its KPIs, sell-through and (worse) its
6284
+ * block/unblock target sets from that. `/v1/events/:key/objects` returns the
6285
+ * unprojected snapshot the control-room read model already trusts.
6286
+ */
6254
6287
  objects(key) {
6255
- return this.pub(`/pub/events/${encodeURIComponent(key)}/objects`);
6288
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/objects`);
6289
+ }
6290
+ /**
6291
+ * Exchange the manage token for a one-use organizer socket ticket.
6292
+ *
6293
+ * A browser `WebSocket` cannot send an Authorization header, so the socket's
6294
+ * scope is established here, over ordinary HTTPS. Without it the DO treats a
6295
+ * manager socket as an anonymous public buyer and projects its deltas — so a
6296
+ * hold inside a private allocation is structurally suppressed and the map
6297
+ * drifts away from the truth `objects()` just established.
6298
+ *
6299
+ * Tickets are single-redemption and expire in ~30s: mint one per connect.
6300
+ */
6301
+ subscribeTicket(key) {
6302
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/subscribe-tickets`, { method: "POST" });
6256
6303
  }
6257
6304
  socketUrl(key) {
6258
6305
  return `${this.base.replace(/^http/, "ws")}/pub/events/${encodeURIComponent(key)}/subscribe?surface=manager`;
@@ -6749,7 +6796,7 @@ var ChannelsMode = class {
6749
6796
  limit: 1e3
6750
6797
  });
6751
6798
  for (const row of res.allocations) {
6752
- if (row.channelId && row.channelId !== PUBLIC_CHANNEL_ID) next.set(row.label, row.channelId);
6799
+ if (!isPublicChannelId(row.channelId)) next.set(row.label, row.channelId);
6753
6800
  }
6754
6801
  this.assignmentVersion = res.assignmentVersion;
6755
6802
  if (!res.nextAfterLabel) break;
@@ -6759,8 +6806,13 @@ var ChannelsMode = class {
6759
6806
  }
6760
6807
  // ---- lookups --------------------------------------------------------------
6761
6808
  channelById(id) {
6762
- if (id === PUBLIC_CHANNEL_ID) {
6763
- return { id, name: this.list?.publicSale.name ?? PUBLIC_CHANNEL_NAME, marker: "P", color: null };
6809
+ if (isPublicChannelId(id)) {
6810
+ return {
6811
+ id: PUBLIC_CHANNEL_ID,
6812
+ name: this.list?.publicSale?.name ?? PUBLIC_CHANNEL_NAME,
6813
+ marker: "P",
6814
+ color: null
6815
+ };
6764
6816
  }
6765
6817
  const found = this.list?.channels.find((channel) => channel.id === id);
6766
6818
  return found ? { id, name: found.name, marker: found.marker, color: found.color } : null;
@@ -6981,7 +7033,10 @@ var ChannelsMode = class {
6981
7033
  this.setBanner(true, names);
6982
7034
  try {
6983
7035
  this.previewProjection = await this.host.api.channelPreview(this.host.eventKey, audience, {
6984
- includePublic: this.previewIncludePublic
7036
+ // Naming Public sale as the audience IS asking for public inventory; the
7037
+ // route filters the 'public' sentinel out of `channelIds`, so without
7038
+ // this the request would resolve to an empty scope and 422.
7039
+ includePublic: this.previewIncludePublic || audience.some(isPublicChannelId)
6985
7040
  });
6986
7041
  this.previewSupported = true;
6987
7042
  } catch (err) {
@@ -7182,7 +7237,7 @@ var ChannelsMode = class {
7182
7237
  const counts = this.previewProjection?.counts;
7183
7238
  const summary = counts?.eligible != null && this.previewProjection?.available !== false ? `<div class="slm-ch-alert warn"><span>\u2139</span><span>${counts.eligible.toLocaleString()} seats are buyable
7184
7239
  through this access.${this.previewProjection?.includePublic === false ? " Public sale seats are <b>not</b> included in this grant." : ""}</span></div>` : "";
7185
- const includePublic = current === PUBLIC_CHANNEL_ID ? "" : `
7240
+ const includePublic = isPublicChannelId(current) ? "" : `
7186
7241
  <label class="slm-note" style="display:flex;gap:8px;align-items:center;margin:10px 0">
7187
7242
  <input type="checkbox" data-ch-includepublic ${this.previewIncludePublic ? "checked" : ""} />
7188
7243
  Also include Public sale seats in this grant
@@ -7394,7 +7449,7 @@ var ChannelsMode = class {
7394
7449
  this.lastFocus = null;
7395
7450
  }
7396
7451
  renderCreateDialog(state) {
7397
- const taken = (this.list?.channels ?? []).map((channel) => channel.marker ?? channel.name[0] ?? "");
7452
+ const taken = (this.list?.channels ?? []).map((channel) => markerLetter(channel.marker || channel.name, ""));
7398
7453
  const suggestion = suggestMarker("", taken);
7399
7454
  this.renderScrim(`
7400
7455
  <h3 id="slm-ch-dlg-title">Create channel</h3>
@@ -7452,7 +7507,8 @@ var ChannelsMode = class {
7452
7507
  try {
7453
7508
  const res = await this.host.api.createChannel(this.host.eventKey, {
7454
7509
  name: trimmed,
7455
- marker: letter || null,
7510
+ // The column is free text; we only ever store what the chip can draw.
7511
+ marker: markerLetter(letter, "") || null,
7456
7512
  color: color || null,
7457
7513
  externalRef: externalRef.trim() || null
7458
7514
  });
@@ -7521,7 +7577,9 @@ var ChannelsMode = class {
7521
7577
  this.renderDialog();
7522
7578
  try {
7523
7579
  const result = await this.host.api.applyChannelAssignment(this.host.eventKey, {
7524
- targetChannelId: this.targetChannelId || null,
7580
+ // `null` is the wire spelling of "back to public sale" every worker
7581
+ // accepts, including ones that predate the 'public' sentinel.
7582
+ targetChannelId: isPublicChannelId(this.targetChannelId) ? null : this.targetChannelId,
7525
7583
  labels,
7526
7584
  assignmentVersion: this.assignmentVersion
7527
7585
  });
@@ -8199,7 +8257,7 @@ var SeatManager = class {
8199
8257
  if (controlRoom?.activity) this.seedFeed(controlRoom.activity);
8200
8258
  else this.api.log(this.key, { limit: 24 }).then((page) => this.seedFeed(page.entries)).catch(() => {
8201
8259
  });
8202
- this.connect();
8260
+ void this.connect();
8203
8261
  this.startFeedClock();
8204
8262
  this.ready = true;
8205
8263
  await this.resolveChannelCapabilities();
@@ -8623,11 +8681,33 @@ var SeatManager = class {
8623
8681
  });
8624
8682
  }
8625
8683
  // ---- realtime -------------------------------------------------------------
8626
- connect() {
8684
+ /**
8685
+ * Open the cockpit's realtime socket AS THE ORGANIZER.
8686
+ *
8687
+ * The scope has to be established before the upgrade, because a browser
8688
+ * `WebSocket` cannot send an Authorization header: the manage token is traded
8689
+ * over HTTPS for a one-use ticket which rides in `Sec-WebSocket-Protocol`.
8690
+ * Without it the server treats this socket as an anonymous public buyer and
8691
+ * projects its deltas, so any change inside a private channel allocation is
8692
+ * structurally suppressed and the map silently drifts.
8693
+ *
8694
+ * If the mint fails (an expired token, a worker that predates the route) we
8695
+ * still connect unticketed rather than going dark — the public-sale stream is
8696
+ * worth having, and every `resnapshot()` re-establishes physical truth from
8697
+ * the authenticated HTTP read.
8698
+ */
8699
+ async connect() {
8700
+ if (this.closed) return;
8701
+ let protocols;
8702
+ try {
8703
+ protocols = (await this.api.subscribeTicket(this.key)).protocols;
8704
+ } catch {
8705
+ protocols = void 0;
8706
+ }
8627
8707
  if (this.closed) return;
8628
8708
  let ws;
8629
8709
  try {
8630
- ws = new WebSocket(this.api.socketUrl(this.key));
8710
+ ws = protocols ? new WebSocket(this.api.socketUrl(this.key), protocols) : new WebSocket(this.api.socketUrl(this.key));
8631
8711
  } catch {
8632
8712
  this.scheduleReconnect();
8633
8713
  return;
@@ -8657,7 +8737,7 @@ var SeatManager = class {
8657
8737
  const delay = Math.min(1e3 * 2 ** Math.min(this.attempt++, 5), 15e3);
8658
8738
  this.reconnectTimer = setTimeout(() => {
8659
8739
  this.reconnectTimer = null;
8660
- this.connect();
8740
+ void this.connect();
8661
8741
  }, delay);
8662
8742
  }
8663
8743
  onMessage(e) {
@@ -8687,7 +8767,7 @@ var SeatManager = class {
8687
8767
  }
8688
8768
  if (m.type === "hidden") return;
8689
8769
  if (m.seats && typeof m.seats === "object") {
8690
- this.applySnapshot(m.seats);
8770
+ this.applySnapshot(m.seats, typeof m.default === "string" ? m.default : void 0);
8691
8771
  } else if (Array.isArray(m.changes)) {
8692
8772
  const ids = [];
8693
8773
  const groups = /* @__PURE__ */ new Map();
@@ -8727,10 +8807,23 @@ var SeatManager = class {
8727
8807
  } catch {
8728
8808
  }
8729
8809
  }
8730
- applySnapshot(seats) {
8810
+ /**
8811
+ * Replace the whole seat model.
8812
+ *
8813
+ * `fallback` is the compact frame's modal status: those snapshots list only
8814
+ * the seats that DIFFER from it, so every other known label takes it. Without
8815
+ * this the omitted majority would silently fall back to `free` — fine when
8816
+ * the mode really is free, wrong the moment it is not.
8817
+ */
8818
+ applySnapshot(seats, fallback) {
8819
+ const known = (st) => ["free", "held", "booked", "blocked"].includes(st) ? st : "free";
8731
8820
  const next = /* @__PURE__ */ new Map();
8821
+ if (fallback !== void 0) {
8822
+ const base = known(fallback);
8823
+ for (const label of this.labelToId.keys()) next.set(label, base);
8824
+ }
8732
8825
  for (const [label, st] of Object.entries(seats)) {
8733
- next.set(label, ["free", "held", "booked", "blocked"].includes(st) ? st : "free");
8826
+ next.set(label, known(st));
8734
8827
  }
8735
8828
  this.status = next;
8736
8829
  this.lastSyncedAt = Date.now();
@@ -9951,6 +10044,8 @@ var SeatManager = class {
9951
10044
  createBuyerAccessContext,
9952
10045
  createControllerSink,
9953
10046
  dropReviewRows,
10047
+ isPublicChannelId,
10048
+ markerLetter,
9954
10049
  markerOf,
9955
10050
  mutationCount,
9956
10051
  needsMoveConfirmation,