@seatlayer/js 0.39.0 → 0.40.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.cjs +806 -727
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +179 -158
- package/dist/index.d.ts +179 -158
- package/dist/index.js +806 -727
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -13,796 +13,875 @@ import {
|
|
|
13
13
|
t
|
|
14
14
|
} from "@seatlayer/core";
|
|
15
15
|
|
|
16
|
-
// src/
|
|
17
|
-
var
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
channel_assignment_conflict: "ineligible",
|
|
31
|
-
allocation_exhausted: "exhausted"
|
|
32
|
-
};
|
|
33
|
-
var PubApi = class {
|
|
34
|
-
constructor(base, options = {}) {
|
|
35
|
-
this.base = base;
|
|
36
|
-
this.viewerId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `viewer_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
37
|
-
this.access = options.access;
|
|
38
|
-
this.onObjectUnavailable = options.onObjectUnavailable;
|
|
39
|
-
}
|
|
40
|
-
/** True when this client is bound to a buyer access session. */
|
|
41
|
-
get accessScoped() {
|
|
42
|
-
return !!this.access?.configured;
|
|
43
|
-
}
|
|
44
|
-
async request(path, init = {}, retried = false) {
|
|
45
|
-
const method = init.method ?? "GET";
|
|
46
|
-
const headers = {};
|
|
47
|
-
let body;
|
|
48
|
-
if (init.body !== void 0) {
|
|
49
|
-
headers["Content-Type"] = "application/json";
|
|
50
|
-
body = JSON.stringify(init.body);
|
|
51
|
-
}
|
|
52
|
-
const authorization = await this.access?.authorization(retried ? "unauthorized" : "initial");
|
|
53
|
-
if (authorization) headers.Authorization = authorization;
|
|
54
|
-
const res = await fetch(`${this.base}${path}`, { method, headers, body, credentials: "omit" });
|
|
55
|
-
const isJson = (res.headers.get("content-type") ?? "").includes("application/json");
|
|
56
|
-
const data = isJson ? await res.json().catch(() => null) : null;
|
|
57
|
-
if (!res.ok) {
|
|
58
|
-
const err = data;
|
|
59
|
-
const code = err?.code ?? err?.error;
|
|
60
|
-
if (this.access?.configured && (res.status === 401 || res.status === 403 || res.status === 422)) {
|
|
61
|
-
const refreshed = await this.access.handleFailure(res.status, code);
|
|
62
|
-
if (refreshed && !retried) return this.request(path, init, true);
|
|
63
|
-
}
|
|
64
|
-
if (res.status === 409) {
|
|
65
|
-
const reason = code ? OBJECT_UNAVAILABLE_CODES[code] : void 0;
|
|
66
|
-
const labels = err?.conflicts?.map((c) => c.label) ?? init.labels ?? [];
|
|
67
|
-
if (reason) this.onObjectUnavailable?.({ labels, reason, code });
|
|
68
|
-
}
|
|
69
|
-
throw new ApiError(
|
|
70
|
-
res.status,
|
|
71
|
-
err?.error ?? `request_failed_${res.status}`,
|
|
72
|
-
code,
|
|
73
|
-
err?.conflicts,
|
|
74
|
-
err?.reason
|
|
75
|
-
);
|
|
16
|
+
// src/buyerRealtime.ts
|
|
17
|
+
var SEATLAYER_V1 = "seatlayer.v1";
|
|
18
|
+
var SEP = "\0";
|
|
19
|
+
var CLOSE_ACCESS_REVOKED = 4401;
|
|
20
|
+
var MAX_BACKOFF_MS = 15e3;
|
|
21
|
+
var PING_INTERVAL_MS = 25e3;
|
|
22
|
+
var PONG_GRACE_MS = 1e4;
|
|
23
|
+
var RESUME_ANSWER_GRACE_MS = 5e3;
|
|
24
|
+
function projectionFromSnapshot(frame) {
|
|
25
|
+
const fallback = typeof frame.default === "string" ? frame.default : "free";
|
|
26
|
+
const exceptions = {};
|
|
27
|
+
if (frame.seats && typeof frame.seats === "object") {
|
|
28
|
+
for (const [label, status] of Object.entries(frame.seats)) {
|
|
29
|
+
if (typeof status === "string" && status !== fallback) exceptions[label] = status;
|
|
76
30
|
}
|
|
77
|
-
return data;
|
|
78
|
-
}
|
|
79
|
-
chart(key) {
|
|
80
|
-
return this.request(`/pub/events/${encodeURIComponent(key)}/chart`);
|
|
81
|
-
}
|
|
82
|
-
objects(key) {
|
|
83
|
-
return this.request(`/pub/events/${encodeURIComponent(key)}/objects`);
|
|
84
31
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
32
|
+
return { default: fallback, exceptions };
|
|
33
|
+
}
|
|
34
|
+
function diffProjections(prev, next) {
|
|
35
|
+
if (!prev || prev.default !== next.default) return null;
|
|
36
|
+
const changes = [];
|
|
37
|
+
for (const [label, status] of Object.entries(next.exceptions)) {
|
|
38
|
+
if (prev.exceptions[label] !== status) changes.push({ label, status });
|
|
91
39
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// the SDK quietly pick venue-wide and hold for the server default instead.
|
|
95
|
-
bestAvailable(key, qty, categoryKey, zoneId, ttlMs) {
|
|
96
|
-
return this.request(`/pub/events/${encodeURIComponent(key)}/best-available`, {
|
|
97
|
-
method: "POST",
|
|
98
|
-
body: { qty, ...categoryKey ? { categoryKey } : {}, ...zoneId ? { zoneId } : {}, ...ttlMs ? { ttlMs } : {} }
|
|
99
|
-
});
|
|
40
|
+
for (const label of Object.keys(prev.exceptions)) {
|
|
41
|
+
if (!(label in next.exceptions)) changes.push({ label, status: next.default });
|
|
100
42
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
43
|
+
return changes;
|
|
44
|
+
}
|
|
45
|
+
function applyChanges(projection, changes) {
|
|
46
|
+
for (const change of changes) {
|
|
47
|
+
if (change.status === projection.default) delete projection.exceptions[change.label];
|
|
48
|
+
else projection.exceptions[change.label] = change.status;
|
|
106
49
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
});
|
|
50
|
+
}
|
|
51
|
+
function assertCredentialFreeUrl(url) {
|
|
52
|
+
if (/(?:^|[?&#])(?:token|access_token|bearer|authorization|ticket|tkt|bse)=/i.test(url)) {
|
|
53
|
+
throw new Error("seatlayer: refusing to open a socket with a credential in the URL");
|
|
112
54
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
extend(key, holdId, ttlMs) {
|
|
116
|
-
return this.request(`/pub/events/${encodeURIComponent(key)}/extend`, {
|
|
117
|
-
method: "POST",
|
|
118
|
-
body: { holdId, ...ttlMs ? { ttlMs } : {} }
|
|
119
|
-
});
|
|
55
|
+
if (/\bbse_[A-Za-z0-9._-]+/.test(url)) {
|
|
56
|
+
throw new Error("seatlayer: refusing to open a socket with a credential in the URL");
|
|
120
57
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
58
|
+
}
|
|
59
|
+
var BuyerRealtimeClient = class {
|
|
60
|
+
constructor(options) {
|
|
61
|
+
this.ws = null;
|
|
62
|
+
this.stopped = true;
|
|
63
|
+
this.attempt = 0;
|
|
64
|
+
this.reconnectTimer = null;
|
|
65
|
+
this.pingTimer = null;
|
|
66
|
+
this.pongTimer = null;
|
|
67
|
+
this.resumeTimer = null;
|
|
68
|
+
/** Our model of this scope's projection. Null until the first snapshot. */
|
|
69
|
+
this.projection = null;
|
|
70
|
+
/** Last `snapshotVersion` seen on any frame that carried one — the resume point. */
|
|
71
|
+
this.version = null;
|
|
72
|
+
/** True once the 101 echoed `seatlayer.v1`. */
|
|
73
|
+
this.v1 = false;
|
|
74
|
+
/** Set when we offered v1 and the handshake came back without it — a proxy
|
|
75
|
+
* most likely stripped the header, so the next attempt selects the v1 frame
|
|
76
|
+
* format with the `?pv=1` marker instead (protocol doc §1). The marker
|
|
77
|
+
* selects a format and can never carry a credential or widen a scope. */
|
|
78
|
+
this.useQueryMarker = false;
|
|
79
|
+
this.hidden = null;
|
|
80
|
+
this.closedSections = null;
|
|
81
|
+
this.opts = options;
|
|
82
|
+
assertCredentialFreeUrl(options.url);
|
|
131
83
|
}
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
* The amount is NOT sent: the server recomputes it from the hold's own items,
|
|
136
|
-
* which is the only reason a browser cannot alter what it pays. Nor is the
|
|
137
|
-
* PROVIDER — the event row decides which gateway charges, and a `provider` in
|
|
138
|
-
* the body is checked rather than obeyed (409 `provider_mismatch`). Omitting
|
|
139
|
-
* it is the shape that cannot disagree.
|
|
140
|
-
*/
|
|
141
|
-
startCheckout(key, input) {
|
|
142
|
-
return this.request(`/pub/events/${encodeURIComponent(key)}/checkout`, {
|
|
143
|
-
method: "POST",
|
|
144
|
-
body: input
|
|
145
|
-
});
|
|
84
|
+
/** Negotiated protocol, for tests and diagnostics. */
|
|
85
|
+
get protocol() {
|
|
86
|
+
return this.ws ? this.v1 ? "v1" : "legacy" : null;
|
|
146
87
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
* unguessable token the buyer already holds, so it acts as the capability —
|
|
150
|
-
* which is also why a buyer returning from a gateway page can be told what
|
|
151
|
-
* happened with nothing but the id in the return URL.
|
|
152
|
-
*/
|
|
153
|
-
orderStatus(orderId) {
|
|
154
|
-
return this.request(`/pub/orders/${encodeURIComponent(orderId)}/status`);
|
|
88
|
+
get snapshotVersion() {
|
|
89
|
+
return this.version;
|
|
155
90
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
* subprotocol list. TTL ≤ 30s, single redemption — mint one per attempt.
|
|
161
|
-
*/
|
|
162
|
-
subscribeTicket(key) {
|
|
163
|
-
return this.request(`/pub/events/${encodeURIComponent(key)}/subscribe-tickets`, {
|
|
164
|
-
method: "POST",
|
|
165
|
-
body: {}
|
|
166
|
-
});
|
|
91
|
+
start() {
|
|
92
|
+
if (!this.stopped) return;
|
|
93
|
+
this.stopped = false;
|
|
94
|
+
void this.connect();
|
|
167
95
|
}
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
96
|
+
/** Stop for good (destroy, or a revocation). Safe to call twice. */
|
|
97
|
+
stop() {
|
|
98
|
+
this.stopped = true;
|
|
99
|
+
this.clearTimers();
|
|
100
|
+
const ws = this.ws;
|
|
101
|
+
this.ws = null;
|
|
102
|
+
if (ws) {
|
|
103
|
+
ws.onopen = null;
|
|
104
|
+
ws.onmessage = null;
|
|
105
|
+
ws.onclose = null;
|
|
106
|
+
ws.onerror = null;
|
|
107
|
+
try {
|
|
108
|
+
ws.close();
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
}
|
|
176
112
|
}
|
|
177
|
-
/**
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
* public client returns exactly the URL it always has, so nothing about the
|
|
185
|
-
* public picker's realtime path changes.
|
|
186
|
-
*/
|
|
187
|
-
socketUrl(key) {
|
|
188
|
-
return this.accessScoped ? "" : this.subscribeUrl(key);
|
|
113
|
+
/** Restart after the host re-authorized a revoked buyer (`refreshAccess()`). */
|
|
114
|
+
restart() {
|
|
115
|
+
this.stop();
|
|
116
|
+
this.projection = null;
|
|
117
|
+
this.version = null;
|
|
118
|
+
this.attempt = 0;
|
|
119
|
+
this.start();
|
|
189
120
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
case "buyer_access_invalid":
|
|
211
|
-
return "invalid";
|
|
212
|
-
case "buyer_access_revoked":
|
|
213
|
-
return "revoked";
|
|
214
|
-
case "buyer_access_origin_mismatch":
|
|
215
|
-
return "origin_mismatch";
|
|
216
|
-
case "buyer_access_event_mismatch":
|
|
217
|
-
return "event_mismatch";
|
|
218
|
-
case "buyer_access_mode_mismatch":
|
|
219
|
-
return "mode_mismatch";
|
|
220
|
-
case "channel_access_denied":
|
|
221
|
-
return "channel_denied";
|
|
222
|
-
case "channel_paused":
|
|
223
|
-
return "paused";
|
|
224
|
-
case "invalid_channel_scope":
|
|
225
|
-
return "invalid_scope";
|
|
226
|
-
default:
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
if (status === 401) return "invalid";
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
function isAccessExpiry(status, code) {
|
|
233
|
-
return status === 401 && !!code && EXPIRED_CODES.has(code);
|
|
234
|
-
}
|
|
235
|
-
var DEFAULT_SKEW_MS = 3e4;
|
|
236
|
-
var _token, _expiresAt, _provider, _skewMs, _inflight, _terminal, _lastFailure, _onExpired, _onUnavailable, _configured, _BuyerAccessContext_instances, accept_fn, renew_fn, fail_fn;
|
|
237
|
-
var BuyerAccessContext = class {
|
|
238
|
-
constructor(options) {
|
|
239
|
-
__privateAdd(this, _BuyerAccessContext_instances);
|
|
240
|
-
/** Private field: not enumerable, not spreadable, not serializable. */
|
|
241
|
-
__privateAdd(this, _token, null);
|
|
242
|
-
__privateAdd(this, _expiresAt, 0);
|
|
243
|
-
__privateAdd(this, _provider);
|
|
244
|
-
__privateAdd(this, _skewMs);
|
|
245
|
-
__privateAdd(this, _inflight, null);
|
|
246
|
-
__privateAdd(this, _terminal, null);
|
|
247
|
-
/** The most recent failure, terminal or not — so one cause reports once. */
|
|
248
|
-
__privateAdd(this, _lastFailure, null);
|
|
249
|
-
__privateAdd(this, _onExpired);
|
|
250
|
-
__privateAdd(this, _onUnavailable);
|
|
251
|
-
/** Decided once, at construction. See the `configured` getter. */
|
|
252
|
-
__privateAdd(this, _configured, false);
|
|
253
|
-
__privateSet(this, _provider, options.provider);
|
|
254
|
-
__privateSet(this, _skewMs, options.skewMs ?? DEFAULT_SKEW_MS);
|
|
255
|
-
__privateSet(this, _onExpired, options.onExpired);
|
|
256
|
-
__privateSet(this, _onUnavailable, options.onUnavailable);
|
|
257
|
-
if (options.token) {
|
|
258
|
-
const seed = typeof options.token === "string" ? { token: options.token } : options.token;
|
|
259
|
-
__privateMethod(this, _BuyerAccessContext_instances, accept_fn).call(this, seed);
|
|
121
|
+
// ---- connection -----------------------------------------------------------
|
|
122
|
+
async connect() {
|
|
123
|
+
if (this.stopped) return;
|
|
124
|
+
let protocols = [SEATLAYER_V1];
|
|
125
|
+
if (this.opts.mintTicket) {
|
|
126
|
+
let minted;
|
|
127
|
+
try {
|
|
128
|
+
minted = await this.opts.mintTicket();
|
|
129
|
+
} catch (err) {
|
|
130
|
+
this.reportIfAccessError(err);
|
|
131
|
+
this.scheduleReconnect();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (this.stopped) return;
|
|
135
|
+
if (minted?.protocols?.length) {
|
|
136
|
+
protocols = [...minted.protocols];
|
|
137
|
+
if (!protocols.includes(SEATLAYER_V1)) protocols.unshift(SEATLAYER_V1);
|
|
138
|
+
} else if (minted?.ticket) {
|
|
139
|
+
protocols = [SEATLAYER_V1, `tkt.${minted.ticket}`];
|
|
140
|
+
}
|
|
260
141
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
142
|
+
const offeredResume = this.version !== null;
|
|
143
|
+
if (offeredResume) protocols.push(`sv.${this.version}`);
|
|
144
|
+
const url = this.useQueryMarker ? `${this.opts.url}${this.opts.url.includes("?") ? "&" : "?"}pv=1` : this.opts.url;
|
|
145
|
+
assertCredentialFreeUrl(url);
|
|
146
|
+
let ws;
|
|
147
|
+
try {
|
|
148
|
+
const make = this.opts.socketFactory ?? ((u, p) => new WebSocket(u, p));
|
|
149
|
+
ws = make(url, protocols);
|
|
150
|
+
} catch {
|
|
151
|
+
this.scheduleReconnect();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.ws = ws;
|
|
155
|
+
ws.onopen = () => {
|
|
156
|
+
if (this.ws !== ws) return;
|
|
157
|
+
this.attempt = 0;
|
|
158
|
+
this.v1 = ws.protocol === SEATLAYER_V1;
|
|
159
|
+
if (!this.v1) this.useQueryMarker = true;
|
|
160
|
+
this.startKeepalive(ws);
|
|
161
|
+
if (offeredResume) {
|
|
162
|
+
this.resumeTimer = setTimeout(() => {
|
|
163
|
+
this.resumeTimer = null;
|
|
164
|
+
void this.opts.sink.resync();
|
|
165
|
+
}, RESUME_ANSWER_GRACE_MS);
|
|
166
|
+
} else {
|
|
167
|
+
void this.opts.sink.resync();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
ws.onmessage = (event) => {
|
|
171
|
+
if (this.ws !== ws) return;
|
|
172
|
+
let parsed;
|
|
173
|
+
try {
|
|
174
|
+
parsed = JSON.parse(typeof event.data === "string" ? event.data : "");
|
|
175
|
+
} catch {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!parsed || typeof parsed !== "object") return;
|
|
179
|
+
this.handleFrame(parsed);
|
|
180
|
+
};
|
|
181
|
+
ws.onclose = (event) => {
|
|
182
|
+
if (this.ws !== ws) return;
|
|
183
|
+
this.ws = null;
|
|
184
|
+
this.clearTimers();
|
|
185
|
+
if (event?.code === CLOSE_ACCESS_REVOKED) {
|
|
186
|
+
this.stopped = true;
|
|
187
|
+
this.opts.onAccessUnavailable?.({
|
|
188
|
+
reason: "revoked",
|
|
189
|
+
code: "access_revoked",
|
|
190
|
+
retryable: false
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.scheduleReconnect();
|
|
195
|
+
};
|
|
196
|
+
ws.onerror = () => {
|
|
197
|
+
try {
|
|
198
|
+
ws.close();
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
};
|
|
278
202
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
203
|
+
handleFrame(frame) {
|
|
204
|
+
const type = typeof frame.type === "string" ? frame.type : "";
|
|
205
|
+
if (frame.protocol === 1) this.v1 = true;
|
|
206
|
+
if (typeof frame.snapshotVersion === "number") this.version = frame.snapshotVersion;
|
|
207
|
+
if (type === "pong") {
|
|
208
|
+
this.clearPongTimer();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(frame.hidden) || Array.isArray(frame.closed)) {
|
|
212
|
+
const hidden = Array.isArray(frame.hidden) ? frame.hidden : [];
|
|
213
|
+
const closed = Array.isArray(frame.closed) ? frame.closed : [];
|
|
214
|
+
const hKey = hidden.join(SEP);
|
|
215
|
+
const cKey = closed.join(SEP);
|
|
216
|
+
if (hKey !== this.hidden || cKey !== this.closedSections) {
|
|
217
|
+
this.hidden = hKey;
|
|
218
|
+
this.closedSections = cKey;
|
|
219
|
+
this.opts.sink.onSections?.(hidden, closed);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (type === "hidden") return;
|
|
223
|
+
if (type === "presence") {
|
|
224
|
+
this.opts.sink.onPresence?.({
|
|
225
|
+
shoppingSessions: Number(frame.shoppingSessions) || 0,
|
|
226
|
+
activeHolds: Number(frame.activeHolds) || 0
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (type === "allocation") {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (type === "snapshot" || !type && frame.seats) {
|
|
234
|
+
this.answered();
|
|
235
|
+
const next = projectionFromSnapshot(frame);
|
|
236
|
+
const changes = diffProjections(this.projection, next);
|
|
237
|
+
this.projection = next;
|
|
238
|
+
if (changes === null) void this.opts.sink.resync();
|
|
239
|
+
else if (changes.length) this.opts.sink.applyStatuses(changes);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (type === "delta" && Array.isArray(frame.changes)) {
|
|
243
|
+
this.answered();
|
|
244
|
+
const changes = frame.changes.filter((c) => typeof c?.label === "string" && typeof c?.status === "string").map((c) => ({ label: c.label, status: c.status }));
|
|
245
|
+
if (!changes.length) return;
|
|
246
|
+
if (this.projection) applyChanges(this.projection, changes);
|
|
247
|
+
this.opts.sink.applyStatuses(changes);
|
|
248
|
+
}
|
|
282
249
|
}
|
|
283
|
-
/**
|
|
284
|
-
|
|
285
|
-
|
|
250
|
+
/** The server answered our resume; cancel the fallback resync. */
|
|
251
|
+
answered() {
|
|
252
|
+
if (!this.resumeTimer) return;
|
|
253
|
+
clearTimeout(this.resumeTimer);
|
|
254
|
+
this.resumeTimer = null;
|
|
286
255
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
256
|
+
reportIfAccessError(err) {
|
|
257
|
+
const reason = err?.reason;
|
|
258
|
+
if (err?.name !== "BuyerAccessUnavailableError") return;
|
|
259
|
+
this.stopped = true;
|
|
260
|
+
this.opts.onAccessUnavailable?.({
|
|
261
|
+
reason: reason ?? "invalid",
|
|
262
|
+
code: err.code,
|
|
263
|
+
status: err.status,
|
|
264
|
+
retryable: reason === "paused"
|
|
265
|
+
});
|
|
290
266
|
}
|
|
267
|
+
// ---- keepalive & backoff --------------------------------------------------
|
|
291
268
|
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* anonymous public picker). A configured context either returns a bearer or
|
|
296
|
-
* throws `BuyerAccessUnavailableError` — it never returns null, because a
|
|
297
|
-
* null here would send the request as anonymous Public sale.
|
|
269
|
+
* Liveness is ping/pong, and only ping/pong. A socket that receives nothing
|
|
270
|
+
* for minutes is the normal, correct state for a narrowly-scoped buyer on a
|
|
271
|
+
* busy event (protocol doc §5), so quiet time never triggers a reconnect.
|
|
298
272
|
*/
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const why = __privateGet(this, _token) ? expired ? "expired" : "expiring" : reason;
|
|
307
|
-
const token = await __privateMethod(this, _BuyerAccessContext_instances, renew_fn).call(this, why);
|
|
308
|
-
if (!token) {
|
|
309
|
-
throw new BuyerAccessUnavailableError(
|
|
310
|
-
__privateGet(this, _terminal) ?? __privateGet(this, _lastFailure) ?? __privateMethod(this, _BuyerAccessContext_instances, fail_fn).call(this, "provider_failed")
|
|
311
|
-
);
|
|
273
|
+
startKeepalive(ws) {
|
|
274
|
+
this.pingTimer = setInterval(() => {
|
|
275
|
+
if (this.ws !== ws) return;
|
|
276
|
+
try {
|
|
277
|
+
ws.send(JSON.stringify({ type: "ping" }));
|
|
278
|
+
} catch {
|
|
279
|
+
return;
|
|
312
280
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
281
|
+
this.clearPongTimer();
|
|
282
|
+
this.pongTimer = setTimeout(() => {
|
|
283
|
+
this.pongTimer = null;
|
|
284
|
+
try {
|
|
285
|
+
ws.close();
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
}, PONG_GRACE_MS);
|
|
289
|
+
}, PING_INTERVAL_MS);
|
|
316
290
|
}
|
|
317
291
|
/**
|
|
318
|
-
*
|
|
319
|
-
*
|
|
292
|
+
* FULL jitter, not plain exponential backoff.
|
|
293
|
+
*
|
|
294
|
+
* A deterministic `2**attempt` schedule makes every browser that lost the same
|
|
295
|
+
* socket — a worker redeploy, a DO eviction, a flaky edge PoP — come back in
|
|
296
|
+
* the same millisecond, and an on-sale crowd reconnecting in lockstep is the
|
|
297
|
+
* thing that turns one blip into a self-sustaining thundering herd. Full
|
|
298
|
+
* jitter (`random() * ceiling`) spreads the same crowd across the whole
|
|
299
|
+
* window; the ceiling still doubles, so a persistent outage still backs off.
|
|
300
|
+
*
|
|
301
|
+
* `Math.random` is correct here: this is client code choosing a delay, not a
|
|
302
|
+
* Workflow step that has to replay deterministically.
|
|
320
303
|
*/
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
const reason = classifyAccessFailure(status, code);
|
|
332
|
-
if (reason) {
|
|
333
|
-
__privateMethod(this, _BuyerAccessContext_instances, fail_fn).call(this, reason, code, status);
|
|
334
|
-
return false;
|
|
335
|
-
}
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
/** Host-driven re-acquisition (after the buyer signs in again, say). */
|
|
339
|
-
async refresh(reason = "manual") {
|
|
340
|
-
__privateSet(this, _terminal, null);
|
|
341
|
-
__privateSet(this, _lastFailure, null);
|
|
342
|
-
__privateSet(this, _token, null);
|
|
343
|
-
__privateSet(this, _expiresAt, 0);
|
|
344
|
-
return !!await __privateMethod(this, _BuyerAccessContext_instances, renew_fn).call(this, reason);
|
|
345
|
-
}
|
|
346
|
-
/** Drop the bearer. Called on destroy so nothing outlives the widget. */
|
|
347
|
-
clear() {
|
|
348
|
-
__privateSet(this, _token, null);
|
|
349
|
-
__privateSet(this, _expiresAt, 0);
|
|
350
|
-
__privateSet(this, _inflight, null);
|
|
304
|
+
scheduleReconnect() {
|
|
305
|
+
if (this.stopped || this.reconnectTimer) return;
|
|
306
|
+
const attempt = Math.min(this.attempt++, 5);
|
|
307
|
+
const ceiling = Math.min(1e3 * 2 ** attempt, MAX_BACKOFF_MS);
|
|
308
|
+
const delay = Math.random() * ceiling;
|
|
309
|
+
this.reconnectTimer = setTimeout(() => {
|
|
310
|
+
this.reconnectTimer = null;
|
|
311
|
+
void this.connect();
|
|
312
|
+
}, delay);
|
|
351
313
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
314
|
+
clearPongTimer() {
|
|
315
|
+
if (!this.pongTimer) return;
|
|
316
|
+
clearTimeout(this.pongTimer);
|
|
317
|
+
this.pongTimer = null;
|
|
355
318
|
}
|
|
356
|
-
|
|
357
|
-
|
|
319
|
+
clearTimers() {
|
|
320
|
+
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
321
|
+
this.pingTimer = null;
|
|
322
|
+
this.clearPongTimer();
|
|
323
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
324
|
+
this.reconnectTimer = null;
|
|
325
|
+
if (this.resumeTimer) clearTimeout(this.resumeTimer);
|
|
326
|
+
this.resumeTimer = null;
|
|
358
327
|
}
|
|
359
328
|
};
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
return null;
|
|
329
|
+
function rendererStatus(wire) {
|
|
330
|
+
if (wire === "blocked") return "not_for_sale";
|
|
331
|
+
if (wire === "held" || wire === "booked" || wire === "free" || wire === "not_for_sale") return wire;
|
|
332
|
+
return "free";
|
|
333
|
+
}
|
|
334
|
+
function createControllerSink(controller, options = {}) {
|
|
335
|
+
const idsForLabel = (label) => {
|
|
336
|
+
const table = controller.tableSelection(label);
|
|
337
|
+
if (table) return table.physicalSeatIds;
|
|
338
|
+
const id = controller.idForLabel(label);
|
|
339
|
+
return id ? [id] : [];
|
|
340
|
+
};
|
|
341
|
+
return {
|
|
342
|
+
applyStatuses(changes) {
|
|
343
|
+
const held = controller.currentHold()?.labels ?? [];
|
|
344
|
+
const buckets = {
|
|
345
|
+
free: [],
|
|
346
|
+
held: [],
|
|
347
|
+
booked: [],
|
|
348
|
+
not_for_sale: []
|
|
349
|
+
};
|
|
350
|
+
const flashes = [];
|
|
351
|
+
const lost = [];
|
|
352
|
+
const selected = new Map(controller.getSelection().map((s) => [s.label, s.id]));
|
|
353
|
+
for (const change of changes) {
|
|
354
|
+
const ids = idsForLabel(change.label);
|
|
355
|
+
if (!ids.length) continue;
|
|
356
|
+
const next = rendererStatus(change.status);
|
|
357
|
+
buckets[next].push(...ids);
|
|
358
|
+
if (options.flashOnLiveChange && next !== "free" && !held.includes(change.label) && ids.some((id) => controller.getStatus(id) === "free")) {
|
|
359
|
+
const color = next === "held" ? "#f4b740" : "#f43f5e";
|
|
360
|
+
for (const id of ids) flashes.push({ id, color });
|
|
361
|
+
}
|
|
362
|
+
if (next !== "free" && !held.includes(change.label) && selected.has(change.label)) {
|
|
363
|
+
lost.push(change.label);
|
|
364
|
+
}
|
|
397
365
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
366
|
+
for (const status of ["free", "held", "booked", "not_for_sale"]) {
|
|
367
|
+
if (buckets[status].length) controller.setStatus(buckets[status], status);
|
|
368
|
+
}
|
|
369
|
+
for (const flash of flashes) controller.flashSeat(flash.id, flash.color);
|
|
370
|
+
if (lost.length) {
|
|
371
|
+
const ids = lost.flatMap((label) => idsForLabel(label));
|
|
372
|
+
if (ids.length) controller.deselect(ids);
|
|
373
|
+
const ineligible = changes.some(
|
|
374
|
+
(c) => c.status === "blocked" && lost.includes(c.label)
|
|
375
|
+
);
|
|
376
|
+
options.onSelectedObjectUnavailable?.(lost, ineligible ? "ineligible" : "taken");
|
|
377
|
+
}
|
|
378
|
+
options.onStatusChange?.();
|
|
379
|
+
},
|
|
380
|
+
async resync() {
|
|
381
|
+
await controller.refresh();
|
|
382
|
+
},
|
|
383
|
+
/**
|
|
384
|
+
* Section availability moved. Statuses are re-pulled so the map repaints.
|
|
385
|
+
*
|
|
386
|
+
* Known limit: rebuilding the chart when a section is newly HIDDEN (its
|
|
387
|
+
* seats are stripped, not greyed) lives inside PickerController's own
|
|
388
|
+
* socket handler and has no public entry point, so an access-scoped picker
|
|
389
|
+
* repaints statuses but does not restructure the chart until its next
|
|
390
|
+
* mount. Closing/opening a section — the common mid-sale move — is a
|
|
391
|
+
* status-level change and is handled here in full.
|
|
392
|
+
*/
|
|
393
|
+
onSections(hidden, closed) {
|
|
394
|
+
void controller.refresh();
|
|
395
|
+
options.onSections?.(hidden, closed);
|
|
404
396
|
}
|
|
405
|
-
})();
|
|
406
|
-
__privateSet(this, _inflight, run);
|
|
407
|
-
return run;
|
|
408
|
-
};
|
|
409
|
-
fail_fn = function(reason, code, status) {
|
|
410
|
-
var _a;
|
|
411
|
-
const event = {
|
|
412
|
-
reason,
|
|
413
|
-
code,
|
|
414
|
-
status,
|
|
415
|
-
// Unchanged: `retryable` means "the SAME request may succeed later".
|
|
416
|
-
// `provider_failed` is recoverable but not retryable — the host must fix
|
|
417
|
-
// its mint endpoint first — so the two sets are deliberately different.
|
|
418
|
-
retryable: reason === "paused" || reason === "channel_denied"
|
|
419
397
|
};
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/api.ts
|
|
401
|
+
var ApiError = class extends Error {
|
|
402
|
+
constructor(status, message, code, conflicts, reason, retryAfterS) {
|
|
403
|
+
super(message);
|
|
404
|
+
this.name = "ApiError";
|
|
405
|
+
this.status = status;
|
|
406
|
+
this.code = code;
|
|
407
|
+
this.conflicts = conflicts;
|
|
408
|
+
this.reason = reason;
|
|
409
|
+
this.retryAfterS = retryAfterS;
|
|
425
410
|
}
|
|
426
|
-
(_a = __privateGet(this, _onUnavailable)) == null ? void 0 : _a.call(this, event);
|
|
427
|
-
return event;
|
|
428
411
|
};
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
412
|
+
var MAX_RATE_LIMIT_WAIT_S = 10;
|
|
413
|
+
var DEFAULT_RATE_LIMIT_WAIT_S = 1;
|
|
414
|
+
function parseRetryAfter(header, bodyValue) {
|
|
415
|
+
const raw = (header ?? "").trim();
|
|
416
|
+
if (raw) {
|
|
417
|
+
const seconds = Number(raw);
|
|
418
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds);
|
|
419
|
+
const at = Date.parse(raw);
|
|
420
|
+
if (Number.isFinite(at)) return Math.max(0, Math.ceil((at - Date.now()) / 1e3));
|
|
421
|
+
}
|
|
422
|
+
if (typeof bodyValue === "number" && Number.isFinite(bodyValue) && bodyValue >= 0) {
|
|
423
|
+
return Math.ceil(bodyValue);
|
|
424
|
+
}
|
|
425
|
+
return void 0;
|
|
436
426
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
var
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
function
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
427
|
+
var OBJECT_UNAVAILABLE_CODES = {
|
|
428
|
+
seat_conflict: "taken",
|
|
429
|
+
conflict: "taken",
|
|
430
|
+
channel_assignment_conflict: "ineligible",
|
|
431
|
+
allocation_exhausted: "exhausted"
|
|
432
|
+
};
|
|
433
|
+
var PubApi = class {
|
|
434
|
+
constructor(base, options = {}) {
|
|
435
|
+
this.base = base;
|
|
436
|
+
this.viewerId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `viewer_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
437
|
+
this.access = options.access;
|
|
438
|
+
this.onObjectUnavailable = options.onObjectUnavailable;
|
|
439
|
+
}
|
|
440
|
+
/** True when this client is bound to a buyer access session. */
|
|
441
|
+
get accessScoped() {
|
|
442
|
+
return !!this.access?.configured;
|
|
443
|
+
}
|
|
444
|
+
async request(path, init = {}, retried = {}) {
|
|
445
|
+
const method = init.method ?? "GET";
|
|
446
|
+
const headers = {};
|
|
447
|
+
let body;
|
|
448
|
+
if (init.body !== void 0) {
|
|
449
|
+
headers["Content-Type"] = "application/json";
|
|
450
|
+
body = JSON.stringify(init.body);
|
|
451
|
+
}
|
|
452
|
+
const authorization = await this.access?.authorization(retried.auth ? "unauthorized" : "initial");
|
|
453
|
+
if (authorization) headers.Authorization = authorization;
|
|
454
|
+
const res = await fetch(`${this.base}${path}`, { method, headers, body, credentials: "omit" });
|
|
455
|
+
const isJson = (res.headers.get("content-type") ?? "").includes("application/json");
|
|
456
|
+
const data = isJson ? await res.json().catch(() => null) : null;
|
|
457
|
+
if (!res.ok) {
|
|
458
|
+
const err = data;
|
|
459
|
+
const code = err?.code ?? err?.error;
|
|
460
|
+
if (this.access?.configured && (res.status === 401 || res.status === 403 || res.status === 422)) {
|
|
461
|
+
const refreshed = await this.access.handleFailure(res.status, code);
|
|
462
|
+
if (refreshed && !retried.auth) return this.request(path, init, { ...retried, auth: true });
|
|
463
|
+
}
|
|
464
|
+
if (res.status === 409) {
|
|
465
|
+
const reason = code ? OBJECT_UNAVAILABLE_CODES[code] : void 0;
|
|
466
|
+
const labels = err?.conflicts?.map((c) => c.label) ?? init.labels ?? [];
|
|
467
|
+
if (reason) this.onObjectUnavailable?.({ labels, reason, code });
|
|
468
|
+
}
|
|
469
|
+
let retryAfterS;
|
|
470
|
+
if (res.status === 429) {
|
|
471
|
+
retryAfterS = parseRetryAfter(res.headers.get("Retry-After"), err?.retryAfterSeconds) ?? DEFAULT_RATE_LIMIT_WAIT_S;
|
|
472
|
+
if (method === "GET" && !retried.rateLimit && retryAfterS <= MAX_RATE_LIMIT_WAIT_S) {
|
|
473
|
+
await new Promise((resolve) => setTimeout(resolve, retryAfterS * 1e3));
|
|
474
|
+
return this.request(path, init, { ...retried, rateLimit: true });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
throw new ApiError(
|
|
478
|
+
res.status,
|
|
479
|
+
err?.error ?? `request_failed_${res.status}`,
|
|
480
|
+
code,
|
|
481
|
+
err?.conflicts,
|
|
482
|
+
err?.reason,
|
|
483
|
+
retryAfterS
|
|
484
|
+
);
|
|
452
485
|
}
|
|
486
|
+
return data;
|
|
453
487
|
}
|
|
454
|
-
|
|
455
|
-
}
|
|
456
|
-
function diffProjections(prev, next) {
|
|
457
|
-
if (!prev || prev.default !== next.default) return null;
|
|
458
|
-
const changes = [];
|
|
459
|
-
for (const [label, status] of Object.entries(next.exceptions)) {
|
|
460
|
-
if (prev.exceptions[label] !== status) changes.push({ label, status });
|
|
488
|
+
chart(key) {
|
|
489
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/chart`);
|
|
461
490
|
}
|
|
462
|
-
|
|
463
|
-
|
|
491
|
+
objects(key) {
|
|
492
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/objects`);
|
|
464
493
|
}
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
494
|
+
hold(key, selections, ttlMs, replaceHoldId) {
|
|
495
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/hold`, {
|
|
496
|
+
method: "POST",
|
|
497
|
+
body: { selections, ...ttlMs ? { ttlMs } : {}, ...replaceHoldId ? { replaceHoldId } : {} },
|
|
498
|
+
labels: selections.map((s) => s.label)
|
|
499
|
+
});
|
|
471
500
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
501
|
+
// `zoneId` scopes the pick to one zone and `ttlMs` carries the host's checkout
|
|
502
|
+
// window — both are part of the route contract, and dropping either here made
|
|
503
|
+
// the SDK quietly pick venue-wide and hold for the server default instead.
|
|
504
|
+
bestAvailable(key, qty, categoryKey, zoneId, ttlMs) {
|
|
505
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/best-available`, {
|
|
506
|
+
method: "POST",
|
|
507
|
+
body: { qty, ...categoryKey ? { categoryKey } : {}, ...zoneId ? { zoneId } : {}, ...ttlMs ? { ttlMs } : {} }
|
|
508
|
+
});
|
|
476
509
|
}
|
|
477
|
-
|
|
478
|
-
|
|
510
|
+
resume(key, holdId) {
|
|
511
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/hold/resume`, {
|
|
512
|
+
method: "POST",
|
|
513
|
+
body: { holdId }
|
|
514
|
+
});
|
|
479
515
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
this.attempt = 0;
|
|
486
|
-
this.reconnectTimer = null;
|
|
487
|
-
this.pingTimer = null;
|
|
488
|
-
this.pongTimer = null;
|
|
489
|
-
this.resumeTimer = null;
|
|
490
|
-
/** Our model of this scope's projection. Null until the first snapshot. */
|
|
491
|
-
this.projection = null;
|
|
492
|
-
/** Last `snapshotVersion` seen on any frame that carried one — the resume point. */
|
|
493
|
-
this.version = null;
|
|
494
|
-
/** True once the 101 echoed `seatlayer.v1`. */
|
|
495
|
-
this.v1 = false;
|
|
496
|
-
/** Set when we offered v1 and the handshake came back without it — a proxy
|
|
497
|
-
* most likely stripped the header, so the next attempt selects the v1 frame
|
|
498
|
-
* format with the `?pv=1` marker instead (protocol doc §1). The marker
|
|
499
|
-
* selects a format and can never carry a credential or widen a scope. */
|
|
500
|
-
this.useQueryMarker = false;
|
|
501
|
-
this.hidden = null;
|
|
502
|
-
this.closedSections = null;
|
|
503
|
-
this.opts = options;
|
|
504
|
-
assertCredentialFreeUrl(options.url);
|
|
516
|
+
release(key, labels, holdId) {
|
|
517
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/release`, {
|
|
518
|
+
method: "POST",
|
|
519
|
+
body: { labels, holdId }
|
|
520
|
+
});
|
|
505
521
|
}
|
|
506
|
-
/**
|
|
507
|
-
|
|
508
|
-
|
|
522
|
+
/** P4 "need more time?": push an active hold's expiry out. Throws ApiError 409
|
|
523
|
+
* (reason: expired | extend_limit | not_found | not_active) if it can't. */
|
|
524
|
+
extend(key, holdId, ttlMs) {
|
|
525
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/extend`, {
|
|
526
|
+
method: "POST",
|
|
527
|
+
body: { holdId, ...ttlMs ? { ttlMs } : {} }
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Which gateways this event can actually take money through — the question
|
|
532
|
+
* `checkout: 'hosted'` has to answer BEFORE it shows a buyer a Pay button, so
|
|
533
|
+
* the answer is never discovered by failing a payment.
|
|
534
|
+
*
|
|
535
|
+
* Anonymous, and it discloses no account, key, mode or currency for a gateway
|
|
536
|
+
* that did not match.
|
|
537
|
+
*/
|
|
538
|
+
paymentOptions(key) {
|
|
539
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/payment-options`);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Turn a live hold into an order and start a payment.
|
|
543
|
+
*
|
|
544
|
+
* The amount is NOT sent: the server recomputes it from the hold's own items,
|
|
545
|
+
* which is the only reason a browser cannot alter what it pays. Nor is the
|
|
546
|
+
* PROVIDER — the event row decides which gateway charges, and a `provider` in
|
|
547
|
+
* the body is checked rather than obeyed (409 `provider_mismatch`). Omitting
|
|
548
|
+
* it is the shape that cannot disagree.
|
|
549
|
+
*/
|
|
550
|
+
startCheckout(key, input) {
|
|
551
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/checkout`, {
|
|
552
|
+
method: "POST",
|
|
553
|
+
body: input
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Poll an order while its gateway webhook lands. The order id is an
|
|
558
|
+
* unguessable token the buyer already holds, so it acts as the capability —
|
|
559
|
+
* which is also why a buyer returning from a gateway page can be told what
|
|
560
|
+
* happened with nothing but the id in the return URL.
|
|
561
|
+
*/
|
|
562
|
+
orderStatus(orderId) {
|
|
563
|
+
return this.request(`/pub/orders/${encodeURIComponent(orderId)}/status`);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Mint a one-use subscribe ticket for the next socket attempt (protocol doc
|
|
567
|
+
* §3). The bearer travels here, over ordinary HTTPS where CORS and Origin
|
|
568
|
+
* already apply; the socket then carries only the short-lived ticket, in its
|
|
569
|
+
* subprotocol list. TTL ≤ 30s, single redemption — mint one per attempt.
|
|
570
|
+
*/
|
|
571
|
+
subscribeTicket(key) {
|
|
572
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/subscribe-tickets`, {
|
|
573
|
+
method: "POST",
|
|
574
|
+
body: {}
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* The subscribe URL. Never carries a credential — not the bearer, not the
|
|
579
|
+
* ticket. Query parameters are diagnostics only.
|
|
580
|
+
*/
|
|
581
|
+
subscribeUrl(key) {
|
|
582
|
+
const wsBase = this.base.replace(/^http/, "ws");
|
|
583
|
+
const params = new URLSearchParams({ surface: "picker", viewerId: this.viewerId });
|
|
584
|
+
return `${wsBase}/pub/events/${encodeURIComponent(key)}/subscribe?${params}`;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* What PickerController opens its own socket with.
|
|
588
|
+
*
|
|
589
|
+
* Empty for an access-scoped client: a private scope authenticates with a
|
|
590
|
+
* subprotocol ticket, which a URL-only constructor cannot carry, so the SDK's
|
|
591
|
+
* BuyerRealtimeClient owns that socket instead and the controller skips its
|
|
592
|
+
* own (an empty URL is its documented "no live feed" contract). A tokenless
|
|
593
|
+
* public client returns exactly the URL it always has, so nothing about the
|
|
594
|
+
* public picker's realtime path changes.
|
|
595
|
+
*/
|
|
596
|
+
socketUrl(key) {
|
|
597
|
+
return this.accessScoped ? "" : this.subscribeUrl(key);
|
|
509
598
|
}
|
|
510
|
-
|
|
511
|
-
|
|
599
|
+
/**
|
|
600
|
+
* The subprotocol list a PLAIN `new WebSocket(url, protocols)` must offer for
|
|
601
|
+
* this transport — `PickerTransport.socketProtocols`, which PickerController
|
|
602
|
+
* calls optionally and which nothing implemented until now.
|
|
603
|
+
*
|
|
604
|
+
* Offering `seatlayer.v1` is the whole point: without it the DO answers an
|
|
605
|
+
* anonymous socket with the LEGACY verbose frame — every unit of a 10k-seat
|
|
606
|
+
* event, on connect and on every reconnect — instead of the compact
|
|
607
|
+
* `{default, exceptions}` form. Empty for an access-scoped client, which
|
|
608
|
+
* authenticates with a one-use ticket a URL-only constructor cannot carry and
|
|
609
|
+
* whose socket BuyerRealtimeClient owns instead (see `socketUrl`).
|
|
610
|
+
*
|
|
611
|
+
* `createRealtime` below is the preferred path and supersedes this for any
|
|
612
|
+
* host that can use it; this stays the correct answer for a host that builds
|
|
613
|
+
* the socket itself from the transport contract.
|
|
614
|
+
*/
|
|
615
|
+
socketProtocols(key) {
|
|
616
|
+
void key;
|
|
617
|
+
return this.accessScoped ? [] : [SEATLAYER_V1];
|
|
512
618
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
619
|
+
/**
|
|
620
|
+
* Hand PickerController the v1 realtime client instead of letting it open a
|
|
621
|
+
* bare socket — `PickerTransport.createRealtime`.
|
|
622
|
+
*
|
|
623
|
+
* This is what puts an ANONYMOUS buyer (the on-sale case) on the same wire as
|
|
624
|
+
* a private-channel one: compact snapshots, `sv.<n>` resume so a reconnect
|
|
625
|
+
* inside the ring costs a delta rather than a full re-snapshot, ping/pong
|
|
626
|
+
* liveness, and one jittered backoff implementation shared by both. The
|
|
627
|
+
* anonymous case simply passes no `mintTicket` — the `/pub/events/:key/
|
|
628
|
+
* subscribe` upgrade requires no ticket, and the DO resolves a ticketless
|
|
629
|
+
* socket to the public scope.
|
|
630
|
+
*
|
|
631
|
+
* Null when access-scoped: that socket is owned by the widget's own
|
|
632
|
+
* BuyerRealtimeClient (with the ticket exchange), and `socketUrl()` already
|
|
633
|
+
* returns '' so the controller opens nothing.
|
|
634
|
+
*/
|
|
635
|
+
createRealtime(key, sink) {
|
|
636
|
+
if (this.accessScoped) return null;
|
|
637
|
+
return new BuyerRealtimeClient({ url: this.subscribeUrl(key), sink });
|
|
517
638
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
ws.onerror = null;
|
|
529
|
-
try {
|
|
530
|
-
ws.close();
|
|
531
|
-
} catch {
|
|
532
|
-
}
|
|
533
|
-
}
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// src/buyerAccess.ts
|
|
642
|
+
var BuyerAccessUnavailableError = class extends Error {
|
|
643
|
+
constructor(event) {
|
|
644
|
+
super(`buyer_access_unavailable:${event.reason}`);
|
|
645
|
+
this.name = "BuyerAccessUnavailableError";
|
|
646
|
+
this.reason = event.reason;
|
|
647
|
+
this.code = event.code;
|
|
648
|
+
this.status = event.status;
|
|
534
649
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
650
|
+
};
|
|
651
|
+
var EXPIRED_CODES = /* @__PURE__ */ new Set(["buyer_access_expired"]);
|
|
652
|
+
var RECOVERABLE = /* @__PURE__ */ new Set([
|
|
653
|
+
"paused",
|
|
654
|
+
"provider_failed",
|
|
655
|
+
"channel_denied"
|
|
656
|
+
]);
|
|
657
|
+
function classifyAccessFailure(status, code) {
|
|
658
|
+
switch (code) {
|
|
659
|
+
case "buyer_access_invalid":
|
|
660
|
+
return "invalid";
|
|
661
|
+
case "buyer_access_revoked":
|
|
662
|
+
return "revoked";
|
|
663
|
+
case "buyer_access_origin_mismatch":
|
|
664
|
+
return "origin_mismatch";
|
|
665
|
+
case "buyer_access_event_mismatch":
|
|
666
|
+
return "event_mismatch";
|
|
667
|
+
case "buyer_access_mode_mismatch":
|
|
668
|
+
return "mode_mismatch";
|
|
669
|
+
case "channel_access_denied":
|
|
670
|
+
return "channel_denied";
|
|
671
|
+
case "channel_paused":
|
|
672
|
+
return "paused";
|
|
673
|
+
case "invalid_channel_scope":
|
|
674
|
+
return "invalid_scope";
|
|
675
|
+
default:
|
|
676
|
+
break;
|
|
542
677
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
this.
|
|
574
|
-
return;
|
|
678
|
+
if (status === 401) return "invalid";
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
function isAccessExpiry(status, code) {
|
|
682
|
+
return status === 401 && !!code && EXPIRED_CODES.has(code);
|
|
683
|
+
}
|
|
684
|
+
var DEFAULT_SKEW_MS = 3e4;
|
|
685
|
+
var _token, _expiresAt, _provider, _skewMs, _inflight, _terminal, _lastFailure, _onExpired, _onUnavailable, _configured, _BuyerAccessContext_instances, accept_fn, renew_fn, fail_fn;
|
|
686
|
+
var BuyerAccessContext = class {
|
|
687
|
+
constructor(options) {
|
|
688
|
+
__privateAdd(this, _BuyerAccessContext_instances);
|
|
689
|
+
/** Private field: not enumerable, not spreadable, not serializable. */
|
|
690
|
+
__privateAdd(this, _token, null);
|
|
691
|
+
__privateAdd(this, _expiresAt, 0);
|
|
692
|
+
__privateAdd(this, _provider);
|
|
693
|
+
__privateAdd(this, _skewMs);
|
|
694
|
+
__privateAdd(this, _inflight, null);
|
|
695
|
+
__privateAdd(this, _terminal, null);
|
|
696
|
+
/** The most recent failure, terminal or not — so one cause reports once. */
|
|
697
|
+
__privateAdd(this, _lastFailure, null);
|
|
698
|
+
__privateAdd(this, _onExpired);
|
|
699
|
+
__privateAdd(this, _onUnavailable);
|
|
700
|
+
/** Decided once, at construction. See the `configured` getter. */
|
|
701
|
+
__privateAdd(this, _configured, false);
|
|
702
|
+
__privateSet(this, _provider, options.provider);
|
|
703
|
+
__privateSet(this, _skewMs, options.skewMs ?? DEFAULT_SKEW_MS);
|
|
704
|
+
__privateSet(this, _onExpired, options.onExpired);
|
|
705
|
+
__privateSet(this, _onUnavailable, options.onUnavailable);
|
|
706
|
+
if (options.token) {
|
|
707
|
+
const seed = typeof options.token === "string" ? { token: options.token } : options.token;
|
|
708
|
+
__privateMethod(this, _BuyerAccessContext_instances, accept_fn).call(this, seed);
|
|
575
709
|
}
|
|
576
|
-
this
|
|
577
|
-
ws.onopen = () => {
|
|
578
|
-
if (this.ws !== ws) return;
|
|
579
|
-
this.attempt = 0;
|
|
580
|
-
this.v1 = ws.protocol === SEATLAYER_V1;
|
|
581
|
-
if (!this.v1) this.useQueryMarker = true;
|
|
582
|
-
this.startKeepalive(ws);
|
|
583
|
-
if (offeredResume) {
|
|
584
|
-
this.resumeTimer = setTimeout(() => {
|
|
585
|
-
this.resumeTimer = null;
|
|
586
|
-
void this.opts.sink.resync();
|
|
587
|
-
}, RESUME_ANSWER_GRACE_MS);
|
|
588
|
-
} else {
|
|
589
|
-
void this.opts.sink.resync();
|
|
590
|
-
}
|
|
591
|
-
};
|
|
592
|
-
ws.onmessage = (event) => {
|
|
593
|
-
if (this.ws !== ws) return;
|
|
594
|
-
let parsed;
|
|
595
|
-
try {
|
|
596
|
-
parsed = JSON.parse(typeof event.data === "string" ? event.data : "");
|
|
597
|
-
} catch {
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
if (!parsed || typeof parsed !== "object") return;
|
|
601
|
-
this.handleFrame(parsed);
|
|
602
|
-
};
|
|
603
|
-
ws.onclose = (event) => {
|
|
604
|
-
if (this.ws !== ws) return;
|
|
605
|
-
this.ws = null;
|
|
606
|
-
this.clearTimers();
|
|
607
|
-
if (event?.code === CLOSE_ACCESS_REVOKED) {
|
|
608
|
-
this.stopped = true;
|
|
609
|
-
this.opts.onAccessUnavailable?.({
|
|
610
|
-
reason: "revoked",
|
|
611
|
-
code: "access_revoked",
|
|
612
|
-
retryable: false
|
|
613
|
-
});
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
this.scheduleReconnect();
|
|
617
|
-
};
|
|
618
|
-
ws.onerror = () => {
|
|
619
|
-
try {
|
|
620
|
-
ws.close();
|
|
621
|
-
} catch {
|
|
622
|
-
}
|
|
623
|
-
};
|
|
710
|
+
__privateSet(this, _configured, !!__privateGet(this, _provider) || !!__privateGet(this, _token));
|
|
624
711
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
this.closedSections = cKey;
|
|
641
|
-
this.opts.sink.onSections?.(hidden, closed);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
if (type === "hidden") return;
|
|
645
|
-
if (type === "presence") {
|
|
646
|
-
this.opts.sink.onPresence?.({
|
|
647
|
-
shoppingSessions: Number(frame.shoppingSessions) || 0,
|
|
648
|
-
activeHolds: Number(frame.activeHolds) || 0
|
|
649
|
-
});
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
if (type === "allocation") {
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
if (type === "snapshot" || !type && frame.seats) {
|
|
656
|
-
this.answered();
|
|
657
|
-
const next = projectionFromSnapshot(frame);
|
|
658
|
-
const changes = diffProjections(this.projection, next);
|
|
659
|
-
this.projection = next;
|
|
660
|
-
if (changes === null) void this.opts.sink.resync();
|
|
661
|
-
else if (changes.length) this.opts.sink.applyStatuses(changes);
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
if (type === "delta" && Array.isArray(frame.changes)) {
|
|
665
|
-
this.answered();
|
|
666
|
-
const changes = frame.changes.filter((c) => typeof c?.label === "string" && typeof c?.status === "string").map((c) => ({ label: c.label, status: c.status }));
|
|
667
|
-
if (!changes.length) return;
|
|
668
|
-
if (this.projection) applyChanges(this.projection, changes);
|
|
669
|
-
this.opts.sink.applyStatuses(changes);
|
|
670
|
-
}
|
|
712
|
+
/**
|
|
713
|
+
* True when this picker is access-scoped at all. A false here is the
|
|
714
|
+
* tokenless public picker, which must behave exactly as it always has.
|
|
715
|
+
*
|
|
716
|
+
* Answered from what the HOST asked for, never from live token state. It used
|
|
717
|
+
* to be `!!#provider || !!#token`, which quietly inverted this file's central
|
|
718
|
+
* rule for a one-shot `buyerAccessToken` host: `#fail()` clears `#token`, so
|
|
719
|
+
* the first refusal turned a configured context into an "unconfigured" one,
|
|
720
|
+
* `authorization()` then returned null instead of throwing, and the very next
|
|
721
|
+
* call went out with no bearer — the anonymous Public sale fallback this
|
|
722
|
+
* module exists to prevent. A provider host never saw it, because `#provider`
|
|
723
|
+
* held `configured` true. Found against a live worker in the M9 pass.
|
|
724
|
+
*/
|
|
725
|
+
get configured() {
|
|
726
|
+
return __privateGet(this, _configured);
|
|
671
727
|
}
|
|
672
|
-
/**
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
clearTimeout(this.resumeTimer);
|
|
676
|
-
this.resumeTimer = null;
|
|
728
|
+
/** Set once a state arrives that refreshing cannot clear. */
|
|
729
|
+
get unavailable() {
|
|
730
|
+
return __privateGet(this, _terminal);
|
|
677
731
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
status: err.status,
|
|
686
|
-
retryable: reason === "paused"
|
|
687
|
-
});
|
|
732
|
+
/** True while a usable bearer is held (ignores skew). */
|
|
733
|
+
get hasToken() {
|
|
734
|
+
return !!__privateGet(this, _token) && (__privateGet(this, _expiresAt) === 0 || __privateGet(this, _expiresAt) > Date.now());
|
|
735
|
+
}
|
|
736
|
+
/** Epoch ms the current token expires, or 0 when the host didn't say. */
|
|
737
|
+
get expiresAt() {
|
|
738
|
+
return __privateGet(this, _expiresAt);
|
|
688
739
|
}
|
|
689
|
-
// ---- keepalive & backoff --------------------------------------------------
|
|
690
740
|
/**
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
741
|
+
* The `Authorization` header value for a scoped operation.
|
|
742
|
+
*
|
|
743
|
+
* Returns null only when this context is not configured at all (the ordinary
|
|
744
|
+
* anonymous public picker). A configured context either returns a bearer or
|
|
745
|
+
* throws `BuyerAccessUnavailableError` — it never returns null, because a
|
|
746
|
+
* null here would send the request as anonymous Public sale.
|
|
694
747
|
*/
|
|
695
|
-
|
|
696
|
-
this.
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
748
|
+
async authorization(reason = "initial") {
|
|
749
|
+
if (!this.configured) return null;
|
|
750
|
+
if (__privateGet(this, _terminal)) throw new BuyerAccessUnavailableError(__privateGet(this, _terminal));
|
|
751
|
+
const now = Date.now();
|
|
752
|
+
const stale = !__privateGet(this, _token) || __privateGet(this, _expiresAt) > 0 && __privateGet(this, _expiresAt) - __privateGet(this, _skewMs) <= now;
|
|
753
|
+
if (stale) {
|
|
754
|
+
const expired = !!__privateGet(this, _token) && __privateGet(this, _expiresAt) > 0 && __privateGet(this, _expiresAt) <= now;
|
|
755
|
+
const why = __privateGet(this, _token) ? expired ? "expired" : "expiring" : reason;
|
|
756
|
+
const token = await __privateMethod(this, _BuyerAccessContext_instances, renew_fn).call(this, why);
|
|
757
|
+
if (!token) {
|
|
758
|
+
throw new BuyerAccessUnavailableError(
|
|
759
|
+
__privateGet(this, _terminal) ?? __privateGet(this, _lastFailure) ?? __privateMethod(this, _BuyerAccessContext_instances, fail_fn).call(this, "provider_failed")
|
|
760
|
+
);
|
|
702
761
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
try {
|
|
707
|
-
ws.close();
|
|
708
|
-
} catch {
|
|
709
|
-
}
|
|
710
|
-
}, PONG_GRACE_MS);
|
|
711
|
-
}, PING_INTERVAL_MS);
|
|
762
|
+
return `Bearer ${token}`;
|
|
763
|
+
}
|
|
764
|
+
return `Bearer ${__privateGet(this, _token)}`;
|
|
712
765
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
766
|
+
/**
|
|
767
|
+
* Handle a 401/403 from a scoped call. Returns true when the caller should
|
|
768
|
+
* retry the same request once with the refreshed bearer.
|
|
769
|
+
*/
|
|
770
|
+
async handleFailure(status, code) {
|
|
771
|
+
var _a;
|
|
772
|
+
if (!this.configured) return false;
|
|
773
|
+
if (isAccessExpiry(status, code)) {
|
|
774
|
+
__privateSet(this, _token, null);
|
|
775
|
+
__privateSet(this, _expiresAt, 0);
|
|
776
|
+
const token = await __privateMethod(this, _BuyerAccessContext_instances, renew_fn).call(this, "unauthorized", code);
|
|
777
|
+
(_a = __privateGet(this, _onExpired)) == null ? void 0 : _a.call(this, { reason: "unauthorized", code, refreshed: !!token });
|
|
778
|
+
return !!token;
|
|
779
|
+
}
|
|
780
|
+
const reason = classifyAccessFailure(status, code);
|
|
781
|
+
if (reason) {
|
|
782
|
+
__privateMethod(this, _BuyerAccessContext_instances, fail_fn).call(this, reason, code, status);
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
return false;
|
|
721
786
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
this
|
|
787
|
+
/** Host-driven re-acquisition (after the buyer signs in again, say). */
|
|
788
|
+
async refresh(reason = "manual") {
|
|
789
|
+
__privateSet(this, _terminal, null);
|
|
790
|
+
__privateSet(this, _lastFailure, null);
|
|
791
|
+
__privateSet(this, _token, null);
|
|
792
|
+
__privateSet(this, _expiresAt, 0);
|
|
793
|
+
return !!await __privateMethod(this, _BuyerAccessContext_instances, renew_fn).call(this, reason);
|
|
726
794
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
this
|
|
730
|
-
this
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
795
|
+
/** Drop the bearer. Called on destroy so nothing outlives the widget. */
|
|
796
|
+
clear() {
|
|
797
|
+
__privateSet(this, _token, null);
|
|
798
|
+
__privateSet(this, _expiresAt, 0);
|
|
799
|
+
__privateSet(this, _inflight, null);
|
|
800
|
+
}
|
|
801
|
+
/** Redaction: the bearer must not survive a stringify or an interpolation. */
|
|
802
|
+
toJSON() {
|
|
803
|
+
return { configured: this.configured, hasToken: this.hasToken };
|
|
804
|
+
}
|
|
805
|
+
toString() {
|
|
806
|
+
return "[BuyerAccessContext redacted]";
|
|
735
807
|
}
|
|
736
808
|
};
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
for (const status of ["free", "held", "booked", "not_for_sale"]) {
|
|
775
|
-
if (buckets[status].length) controller.setStatus(buckets[status], status);
|
|
776
|
-
}
|
|
777
|
-
for (const flash of flashes) controller.flashSeat(flash.id, flash.color);
|
|
778
|
-
if (lost.length) {
|
|
779
|
-
const ids = lost.flatMap((label) => idsForLabel(label));
|
|
780
|
-
if (ids.length) controller.deselect(ids);
|
|
781
|
-
const ineligible = changes.some(
|
|
782
|
-
(c) => c.status === "blocked" && lost.includes(c.label)
|
|
783
|
-
);
|
|
784
|
-
options.onSelectedObjectUnavailable?.(lost, ineligible ? "ineligible" : "taken");
|
|
809
|
+
_token = new WeakMap();
|
|
810
|
+
_expiresAt = new WeakMap();
|
|
811
|
+
_provider = new WeakMap();
|
|
812
|
+
_skewMs = new WeakMap();
|
|
813
|
+
_inflight = new WeakMap();
|
|
814
|
+
_terminal = new WeakMap();
|
|
815
|
+
_lastFailure = new WeakMap();
|
|
816
|
+
_onExpired = new WeakMap();
|
|
817
|
+
_onUnavailable = new WeakMap();
|
|
818
|
+
_configured = new WeakMap();
|
|
819
|
+
_BuyerAccessContext_instances = new WeakSet();
|
|
820
|
+
// ---- internals ------------------------------------------------------------
|
|
821
|
+
accept_fn = function(next) {
|
|
822
|
+
if (!next || typeof next.token !== "string" || !next.token) return null;
|
|
823
|
+
__privateSet(this, _token, next.token);
|
|
824
|
+
__privateSet(this, _expiresAt, typeof next.expiresAt === "number" ? next.expiresAt : 0);
|
|
825
|
+
return __privateGet(this, _token);
|
|
826
|
+
};
|
|
827
|
+
/**
|
|
828
|
+
* One provider call at a time. Several operations racing an expiry (chart +
|
|
829
|
+
* objects + a socket ticket) must not mint several sessions — the guide's
|
|
830
|
+
* rotate-on-retry rule would revoke the ones they didn't observe.
|
|
831
|
+
*/
|
|
832
|
+
renew_fn = function(reason, code) {
|
|
833
|
+
if (__privateGet(this, _inflight)) return __privateGet(this, _inflight);
|
|
834
|
+
const provider = __privateGet(this, _provider);
|
|
835
|
+
if (!provider) {
|
|
836
|
+
__privateMethod(this, _BuyerAccessContext_instances, fail_fn).call(this, "no_token", code);
|
|
837
|
+
return Promise.resolve(null);
|
|
838
|
+
}
|
|
839
|
+
const run = (async () => {
|
|
840
|
+
try {
|
|
841
|
+
const next = await provider({ reason });
|
|
842
|
+
const token = __privateMethod(this, _BuyerAccessContext_instances, accept_fn).call(this, next);
|
|
843
|
+
if (!token) {
|
|
844
|
+
__privateMethod(this, _BuyerAccessContext_instances, fail_fn).call(this, "provider_failed", code);
|
|
845
|
+
return null;
|
|
785
846
|
}
|
|
786
|
-
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
* Section availability moved. Statuses are re-pulled so the map repaints.
|
|
793
|
-
*
|
|
794
|
-
* Known limit: rebuilding the chart when a section is newly HIDDEN (its
|
|
795
|
-
* seats are stripped, not greyed) lives inside PickerController's own
|
|
796
|
-
* socket handler and has no public entry point, so an access-scoped picker
|
|
797
|
-
* repaints statuses but does not restructure the chart until its next
|
|
798
|
-
* mount. Closing/opening a section — the common mid-sale move — is a
|
|
799
|
-
* status-level change and is handled here in full.
|
|
800
|
-
*/
|
|
801
|
-
onSections(hidden, closed) {
|
|
802
|
-
void controller.refresh();
|
|
803
|
-
options.onSections?.(hidden, closed);
|
|
847
|
+
return token;
|
|
848
|
+
} catch {
|
|
849
|
+
__privateMethod(this, _BuyerAccessContext_instances, fail_fn).call(this, "provider_failed", code);
|
|
850
|
+
return null;
|
|
851
|
+
} finally {
|
|
852
|
+
__privateSet(this, _inflight, null);
|
|
804
853
|
}
|
|
854
|
+
})();
|
|
855
|
+
__privateSet(this, _inflight, run);
|
|
856
|
+
return run;
|
|
857
|
+
};
|
|
858
|
+
fail_fn = function(reason, code, status) {
|
|
859
|
+
var _a;
|
|
860
|
+
const event = {
|
|
861
|
+
reason,
|
|
862
|
+
code,
|
|
863
|
+
status,
|
|
864
|
+
// Unchanged: `retryable` means "the SAME request may succeed later".
|
|
865
|
+
// `provider_failed` is recoverable but not retryable — the host must fix
|
|
866
|
+
// its mint endpoint first — so the two sets are deliberately different.
|
|
867
|
+
retryable: reason === "paused" || reason === "channel_denied"
|
|
805
868
|
};
|
|
869
|
+
__privateSet(this, _lastFailure, event);
|
|
870
|
+
if (!RECOVERABLE.has(reason)) {
|
|
871
|
+
__privateSet(this, _terminal, event);
|
|
872
|
+
__privateSet(this, _token, null);
|
|
873
|
+
__privateSet(this, _expiresAt, 0);
|
|
874
|
+
}
|
|
875
|
+
(_a = __privateGet(this, _onUnavailable)) == null ? void 0 : _a.call(this, event);
|
|
876
|
+
return event;
|
|
877
|
+
};
|
|
878
|
+
function createBuyerAccessContext(options, hooks = {}) {
|
|
879
|
+
if (!options.buyerAccessTokenProvider && !options.buyerAccessToken) return null;
|
|
880
|
+
return new BuyerAccessContext({
|
|
881
|
+
provider: options.buyerAccessTokenProvider,
|
|
882
|
+
token: options.buyerAccessToken,
|
|
883
|
+
...hooks
|
|
884
|
+
});
|
|
806
885
|
}
|
|
807
886
|
|
|
808
887
|
// src/seatLayerBrand.ts
|