@seatlayer/js 0.36.1 → 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.js CHANGED
@@ -5945,8 +5945,11 @@ import {
5945
5945
  } from "@seatlayer/core";
5946
5946
 
5947
5947
  // src/channelPlan.ts
5948
- var PUBLIC_CHANNEL_ID = "";
5948
+ var PUBLIC_CHANNEL_ID = "public";
5949
5949
  var PUBLIC_CHANNEL_NAME = "Public sale";
5950
+ function isPublicChannelId(id) {
5951
+ return id == null || id === "" || id === PUBLIC_CHANNEL_ID;
5952
+ }
5950
5953
  var CHANNEL_COLORS = [
5951
5954
  "#a78bfa",
5952
5955
  "#2dd4bf",
@@ -5961,27 +5964,35 @@ var CHANNEL_COLORS = [
5961
5964
  ];
5962
5965
  var PUBLIC_CHANNEL_COLOR = "#f4b740";
5963
5966
  var LETTERS = "ABCDEFGHJKLMNPQRSTUVWXYZ";
5967
+ function markerLetter(raw, fallback) {
5968
+ const text = (raw ?? "").trim();
5969
+ const letter = /\p{L}/u.exec(text)?.[0] ?? text[0] ?? "";
5970
+ return (letter || fallback).toUpperCase().slice(0, 1);
5971
+ }
5964
5972
  function suggestMarker(name, taken) {
5965
- const used = new Set([...taken].map((m) => m.trim().toUpperCase()).filter(Boolean));
5966
- const first = (name.trim()[0] ?? "").toUpperCase();
5973
+ const used = new Set([...taken].map((m) => markerLetter(m, "")).filter(Boolean));
5974
+ const first = markerLetter(name, "");
5967
5975
  const letter = LETTERS.includes(first) && !used.has(first) ? first : [...LETTERS].find((candidate) => !used.has(candidate)) ?? (first || "X");
5968
5976
  return { letter, color: CHANNEL_COLORS[used.size % CHANNEL_COLORS.length] };
5969
5977
  }
5970
5978
  function markerOf(channel, index = 0) {
5971
- if (channel.id === PUBLIC_CHANNEL_ID) {
5972
- return { letter: (channel.marker || "P").slice(0, 2).toUpperCase(), color: channel.color || PUBLIC_CHANNEL_COLOR };
5979
+ if (isPublicChannelId(channel.id)) {
5980
+ return {
5981
+ letter: markerLetter(channel.marker, "P"),
5982
+ color: channel.color || PUBLIC_CHANNEL_COLOR
5983
+ };
5973
5984
  }
5974
- const letter = (channel.marker || channel.name.trim()[0] || "?").slice(0, 2).toUpperCase();
5985
+ const letter = markerLetter(channel.marker || channel.name, "?");
5975
5986
  return { letter, color: channel.color || CHANNEL_COLORS[index % CHANNEL_COLORS.length] };
5976
5987
  }
5977
5988
  function selectionSources(labels, allocation, list) {
5978
5989
  const counts = /* @__PURE__ */ new Map();
5979
5990
  for (const label of labels) {
5980
- const channelId = allocation.get(label) ?? PUBLIC_CHANNEL_ID;
5991
+ const channelId = normalizeChannelId(allocation.get(label));
5981
5992
  counts.set(channelId, (counts.get(channelId) ?? 0) + 1);
5982
5993
  }
5983
5994
  const order = [
5984
- { id: PUBLIC_CHANNEL_ID, name: list?.publicSale.name ?? PUBLIC_CHANNEL_NAME },
5995
+ { id: PUBLIC_CHANNEL_ID, name: list?.publicSale?.name ?? PUBLIC_CHANNEL_NAME },
5985
5996
  ...(list?.channels ?? []).map((channel) => ({ id: channel.id, name: channel.name }))
5986
5997
  ];
5987
5998
  const rows = [];
@@ -5991,10 +6002,17 @@ function selectionSources(labels, allocation, list) {
5991
6002
  counts.delete(entry.id);
5992
6003
  }
5993
6004
  for (const [channelId, count] of counts) {
5994
- rows.push({ channelId, name: channelId ? "Another channel" : PUBLIC_CHANNEL_NAME, count });
6005
+ rows.push({
6006
+ channelId,
6007
+ name: isPublicChannelId(channelId) ? PUBLIC_CHANNEL_NAME : "Another channel",
6008
+ count
6009
+ });
5995
6010
  }
5996
6011
  return rows;
5997
6012
  }
6013
+ function normalizeChannelId(id) {
6014
+ return isPublicChannelId(id) ? PUBLIC_CHANNEL_ID : id;
6015
+ }
5998
6016
  var SKIP_SAMPLE = 12;
5999
6017
  function skipBucket(labels) {
6000
6018
  return {
@@ -6004,7 +6022,8 @@ function skipBucket(labels) {
6004
6022
  };
6005
6023
  }
6006
6024
  function planAssignment(input) {
6007
- const { labels, targetChannelId, allocation, statusOf, nameOf } = input;
6025
+ const { labels, allocation, statusOf, nameOf } = input;
6026
+ const targetChannelId = normalizeChannelId(input.targetChannelId);
6008
6027
  const seen = /* @__PURE__ */ new Set();
6009
6028
  let fromPublic = 0;
6010
6029
  let alreadyIn = 0;
@@ -6020,7 +6039,7 @@ function planAssignment(input) {
6020
6039
  missing.push(label);
6021
6040
  continue;
6022
6041
  }
6023
- const current = allocation.get(label) ?? PUBLIC_CHANNEL_ID;
6042
+ const current = normalizeChannelId(allocation.get(label));
6024
6043
  if (current === targetChannelId) {
6025
6044
  alreadyIn += 1;
6026
6045
  continue;
@@ -6203,12 +6222,38 @@ var ManageApi = class {
6203
6222
  pub(path) {
6204
6223
  return fetch(`${this.base}${path}`, { credentials: "omit" }).then((r) => parse(r));
6205
6224
  }
6206
- // ---- realtime read (public, no token) ----
6225
+ // ---- realtime read ----
6226
+ /** The chart geometry. Genuinely public — it is the same map buyers see. */
6207
6227
  chart(key) {
6208
6228
  return this.pub(`/pub/events/${encodeURIComponent(key)}/chart`);
6209
6229
  }
6230
+ /**
6231
+ * The ORGANIZER's seat map: physical state, token-authed.
6232
+ *
6233
+ * This used to read `/pub/events/:key/objects` with no credential, which
6234
+ * answers with the BUYER projection — every unit the caller may not buy
6235
+ * collapses to a neutral `blocked`. An anonymous caller may buy only Public
6236
+ * sale inventory, so the cockpit rendered every channel-allocated seat as
6237
+ * blocked and then computed its KPIs, sell-through and (worse) its
6238
+ * block/unblock target sets from that. `/v1/events/:key/objects` returns the
6239
+ * unprojected snapshot the control-room read model already trusts.
6240
+ */
6210
6241
  objects(key) {
6211
- return this.pub(`/pub/events/${encodeURIComponent(key)}/objects`);
6242
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/objects`);
6243
+ }
6244
+ /**
6245
+ * Exchange the manage token for a one-use organizer socket ticket.
6246
+ *
6247
+ * A browser `WebSocket` cannot send an Authorization header, so the socket's
6248
+ * scope is established here, over ordinary HTTPS. Without it the DO treats a
6249
+ * manager socket as an anonymous public buyer and projects its deltas — so a
6250
+ * hold inside a private allocation is structurally suppressed and the map
6251
+ * drifts away from the truth `objects()` just established.
6252
+ *
6253
+ * Tickets are single-redemption and expire in ~30s: mint one per connect.
6254
+ */
6255
+ subscribeTicket(key) {
6256
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/subscribe-tickets`, { method: "POST" });
6212
6257
  }
6213
6258
  socketUrl(key) {
6214
6259
  return `${this.base.replace(/^http/, "ws")}/pub/events/${encodeURIComponent(key)}/subscribe?surface=manager`;
@@ -6705,7 +6750,7 @@ var ChannelsMode = class {
6705
6750
  limit: 1e3
6706
6751
  });
6707
6752
  for (const row of res.allocations) {
6708
- if (row.channelId && row.channelId !== PUBLIC_CHANNEL_ID) next.set(row.label, row.channelId);
6753
+ if (!isPublicChannelId(row.channelId)) next.set(row.label, row.channelId);
6709
6754
  }
6710
6755
  this.assignmentVersion = res.assignmentVersion;
6711
6756
  if (!res.nextAfterLabel) break;
@@ -6715,8 +6760,13 @@ var ChannelsMode = class {
6715
6760
  }
6716
6761
  // ---- lookups --------------------------------------------------------------
6717
6762
  channelById(id) {
6718
- if (id === PUBLIC_CHANNEL_ID) {
6719
- return { id, name: this.list?.publicSale.name ?? PUBLIC_CHANNEL_NAME, marker: "P", color: null };
6763
+ if (isPublicChannelId(id)) {
6764
+ return {
6765
+ id: PUBLIC_CHANNEL_ID,
6766
+ name: this.list?.publicSale?.name ?? PUBLIC_CHANNEL_NAME,
6767
+ marker: "P",
6768
+ color: null
6769
+ };
6720
6770
  }
6721
6771
  const found = this.list?.channels.find((channel) => channel.id === id);
6722
6772
  return found ? { id, name: found.name, marker: found.marker, color: found.color } : null;
@@ -6937,7 +6987,10 @@ var ChannelsMode = class {
6937
6987
  this.setBanner(true, names);
6938
6988
  try {
6939
6989
  this.previewProjection = await this.host.api.channelPreview(this.host.eventKey, audience, {
6940
- includePublic: this.previewIncludePublic
6990
+ // Naming Public sale as the audience IS asking for public inventory; the
6991
+ // route filters the 'public' sentinel out of `channelIds`, so without
6992
+ // this the request would resolve to an empty scope and 422.
6993
+ includePublic: this.previewIncludePublic || audience.some(isPublicChannelId)
6941
6994
  });
6942
6995
  this.previewSupported = true;
6943
6996
  } catch (err) {
@@ -7138,7 +7191,7 @@ var ChannelsMode = class {
7138
7191
  const counts = this.previewProjection?.counts;
7139
7192
  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
7140
7193
  through this access.${this.previewProjection?.includePublic === false ? " Public sale seats are <b>not</b> included in this grant." : ""}</span></div>` : "";
7141
- const includePublic = current === PUBLIC_CHANNEL_ID ? "" : `
7194
+ const includePublic = isPublicChannelId(current) ? "" : `
7142
7195
  <label class="slm-note" style="display:flex;gap:8px;align-items:center;margin:10px 0">
7143
7196
  <input type="checkbox" data-ch-includepublic ${this.previewIncludePublic ? "checked" : ""} />
7144
7197
  Also include Public sale seats in this grant
@@ -7350,7 +7403,7 @@ var ChannelsMode = class {
7350
7403
  this.lastFocus = null;
7351
7404
  }
7352
7405
  renderCreateDialog(state) {
7353
- const taken = (this.list?.channels ?? []).map((channel) => channel.marker ?? channel.name[0] ?? "");
7406
+ const taken = (this.list?.channels ?? []).map((channel) => markerLetter(channel.marker || channel.name, ""));
7354
7407
  const suggestion = suggestMarker("", taken);
7355
7408
  this.renderScrim(`
7356
7409
  <h3 id="slm-ch-dlg-title">Create channel</h3>
@@ -7408,7 +7461,8 @@ var ChannelsMode = class {
7408
7461
  try {
7409
7462
  const res = await this.host.api.createChannel(this.host.eventKey, {
7410
7463
  name: trimmed,
7411
- marker: letter || null,
7464
+ // The column is free text; we only ever store what the chip can draw.
7465
+ marker: markerLetter(letter, "") || null,
7412
7466
  color: color || null,
7413
7467
  externalRef: externalRef.trim() || null
7414
7468
  });
@@ -7477,7 +7531,9 @@ var ChannelsMode = class {
7477
7531
  this.renderDialog();
7478
7532
  try {
7479
7533
  const result = await this.host.api.applyChannelAssignment(this.host.eventKey, {
7480
- targetChannelId: this.targetChannelId || null,
7534
+ // `null` is the wire spelling of "back to public sale" every worker
7535
+ // accepts, including ones that predate the 'public' sentinel.
7536
+ targetChannelId: isPublicChannelId(this.targetChannelId) ? null : this.targetChannelId,
7481
7537
  labels,
7482
7538
  assignmentVersion: this.assignmentVersion
7483
7539
  });
@@ -8155,7 +8211,7 @@ var SeatManager = class {
8155
8211
  if (controlRoom?.activity) this.seedFeed(controlRoom.activity);
8156
8212
  else this.api.log(this.key, { limit: 24 }).then((page) => this.seedFeed(page.entries)).catch(() => {
8157
8213
  });
8158
- this.connect();
8214
+ void this.connect();
8159
8215
  this.startFeedClock();
8160
8216
  this.ready = true;
8161
8217
  await this.resolveChannelCapabilities();
@@ -8579,11 +8635,33 @@ var SeatManager = class {
8579
8635
  });
8580
8636
  }
8581
8637
  // ---- realtime -------------------------------------------------------------
8582
- connect() {
8638
+ /**
8639
+ * Open the cockpit's realtime socket AS THE ORGANIZER.
8640
+ *
8641
+ * The scope has to be established before the upgrade, because a browser
8642
+ * `WebSocket` cannot send an Authorization header: the manage token is traded
8643
+ * over HTTPS for a one-use ticket which rides in `Sec-WebSocket-Protocol`.
8644
+ * Without it the server treats this socket as an anonymous public buyer and
8645
+ * projects its deltas, so any change inside a private channel allocation is
8646
+ * structurally suppressed and the map silently drifts.
8647
+ *
8648
+ * If the mint fails (an expired token, a worker that predates the route) we
8649
+ * still connect unticketed rather than going dark — the public-sale stream is
8650
+ * worth having, and every `resnapshot()` re-establishes physical truth from
8651
+ * the authenticated HTTP read.
8652
+ */
8653
+ async connect() {
8654
+ if (this.closed) return;
8655
+ let protocols;
8656
+ try {
8657
+ protocols = (await this.api.subscribeTicket(this.key)).protocols;
8658
+ } catch {
8659
+ protocols = void 0;
8660
+ }
8583
8661
  if (this.closed) return;
8584
8662
  let ws;
8585
8663
  try {
8586
- ws = new WebSocket(this.api.socketUrl(this.key));
8664
+ ws = protocols ? new WebSocket(this.api.socketUrl(this.key), protocols) : new WebSocket(this.api.socketUrl(this.key));
8587
8665
  } catch {
8588
8666
  this.scheduleReconnect();
8589
8667
  return;
@@ -8613,7 +8691,7 @@ var SeatManager = class {
8613
8691
  const delay = Math.min(1e3 * 2 ** Math.min(this.attempt++, 5), 15e3);
8614
8692
  this.reconnectTimer = setTimeout(() => {
8615
8693
  this.reconnectTimer = null;
8616
- this.connect();
8694
+ void this.connect();
8617
8695
  }, delay);
8618
8696
  }
8619
8697
  onMessage(e) {
@@ -8643,7 +8721,7 @@ var SeatManager = class {
8643
8721
  }
8644
8722
  if (m.type === "hidden") return;
8645
8723
  if (m.seats && typeof m.seats === "object") {
8646
- this.applySnapshot(m.seats);
8724
+ this.applySnapshot(m.seats, typeof m.default === "string" ? m.default : void 0);
8647
8725
  } else if (Array.isArray(m.changes)) {
8648
8726
  const ids = [];
8649
8727
  const groups = /* @__PURE__ */ new Map();
@@ -8683,10 +8761,23 @@ var SeatManager = class {
8683
8761
  } catch {
8684
8762
  }
8685
8763
  }
8686
- applySnapshot(seats) {
8764
+ /**
8765
+ * Replace the whole seat model.
8766
+ *
8767
+ * `fallback` is the compact frame's modal status: those snapshots list only
8768
+ * the seats that DIFFER from it, so every other known label takes it. Without
8769
+ * this the omitted majority would silently fall back to `free` — fine when
8770
+ * the mode really is free, wrong the moment it is not.
8771
+ */
8772
+ applySnapshot(seats, fallback) {
8773
+ const known = (st) => ["free", "held", "booked", "blocked"].includes(st) ? st : "free";
8687
8774
  const next = /* @__PURE__ */ new Map();
8775
+ if (fallback !== void 0) {
8776
+ const base = known(fallback);
8777
+ for (const label of this.labelToId.keys()) next.set(label, base);
8778
+ }
8688
8779
  for (const [label, st] of Object.entries(seats)) {
8689
- next.set(label, ["free", "held", "booked", "blocked"].includes(st) ? st : "free");
8780
+ next.set(label, known(st));
8690
8781
  }
8691
8782
  this.status = next;
8692
8783
  this.lastSyncedAt = Date.now();
@@ -9906,6 +9997,8 @@ export {
9906
9997
  createBuyerAccessContext,
9907
9998
  createControllerSink,
9908
9999
  dropReviewRows,
10000
+ isPublicChannelId,
10001
+ markerLetter,
9909
10002
  markerOf,
9910
10003
  mutationCount,
9911
10004
  needsMoveConfirmation,