@central-ticket/queue-sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js ADDED
@@ -0,0 +1,624 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
+ var __decorateClass = (decorators, target, key, kind) => {
23
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
24
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
25
+ if (decorator = decorators[i])
26
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
27
+ if (kind && result) __defProp(target, key, result);
28
+ return result;
29
+ };
30
+
31
+ // src/queue-element.ts
32
+ var queue_element_exports = {};
33
+ __export(queue_element_exports, {
34
+ CentralQElement: () => CentralQElement
35
+ });
36
+ var import_lit, import_decorators, CentralQElement;
37
+ var init_queue_element = __esm({
38
+ "src/queue-element.ts"() {
39
+ "use strict";
40
+ import_lit = require("lit");
41
+ import_decorators = require("lit/decorators.js");
42
+ CentralQElement = class extends import_lit.LitElement {
43
+ constructor() {
44
+ super(...arguments);
45
+ this.status = "LOADING";
46
+ this.position = 0;
47
+ this.ahead = 0;
48
+ }
49
+ render() {
50
+ if (this.status === "ACTIVE") return import_lit.html``;
51
+ return import_lit.html`
52
+ <div class="overlay">
53
+ <div class="modal">
54
+ ${this.status === "LOADING" ? import_lit.html`
55
+ <div class="spinner"></div>
56
+ <h2>Conectando a la fila...</h2>
57
+ ` : ""}
58
+ ${this.status === "WAITING" ? import_lit.html`
59
+ <div class="spinner"></div>
60
+ <h2>Estás en la fila virtual</h2>
61
+ <p>Tu posición: <strong>#${this.position}</strong></p>
62
+ ${this.ahead > 0 ? import_lit.html`<p>
63
+ Hay <strong>${this.ahead}</strong>
64
+ ${this.ahead === 1 ? "persona" : "personas"} delante de
65
+ ti.
66
+ </p>` : import_lit.html`<p>¡Eres el siguiente!</p>`}
67
+ <p style="font-size: 0.85rem; color: #aaa;">
68
+ Por favor, no cierres esta ventana.
69
+ </p>
70
+ ` : ""}
71
+ ${this.status === "EXPIRED" ? import_lit.html`
72
+ <h2 style="color: #f59e0b;">Tu sesión ha expirado</h2>
73
+ <p>El tiempo para completar la compra terminó.</p>
74
+ <button
75
+ style="margin-top: 1rem; padding: 0.75rem 2rem; font-size: 1rem;
76
+ cursor: pointer; border-radius: 8px; border: none;
77
+ background: #3b82f6; color: white;"
78
+ @click=${() => this.onRetry?.()}
79
+ >
80
+ Volver a la fila
81
+ </button>
82
+ ` : ""}
83
+ ${this.status === "ERROR" ? import_lit.html`
84
+ <h2 style="color: #ef4444;">Ocurrió un error de conexión</h2>
85
+ <p>Por favor, recarga la página.</p>
86
+ ` : ""}
87
+ </div>
88
+ </div>
89
+ `;
90
+ }
91
+ };
92
+ CentralQElement.styles = import_lit.css`
93
+ :host {
94
+ display: contents;
95
+ }
96
+ .overlay {
97
+ position: fixed;
98
+ inset: 0;
99
+ background: rgba(0, 0, 0, 0.8);
100
+ backdrop-filter: blur(5px);
101
+ display: flex;
102
+ align-items: center;
103
+ justify-content: center;
104
+ z-index: 9999;
105
+ font-family: system-ui, sans-serif;
106
+ }
107
+ .modal {
108
+ background: #1e1e1e;
109
+ color: white;
110
+ padding: 2rem;
111
+ border-radius: 12px;
112
+ text-align: center;
113
+ max-width: 400px;
114
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
115
+ }
116
+ .spinner {
117
+ margin: 1rem auto;
118
+ width: 40px;
119
+ height: 40px;
120
+ border: 4px solid #333;
121
+ border-top-color: #3b82f6;
122
+ border-radius: 50%;
123
+ animation: spin 1s linear infinite;
124
+ }
125
+ @keyframes spin {
126
+ to {
127
+ transform: rotate(360deg);
128
+ }
129
+ }
130
+ `;
131
+ __decorateClass([
132
+ (0, import_decorators.state)()
133
+ ], CentralQElement.prototype, "status", 2);
134
+ __decorateClass([
135
+ (0, import_decorators.state)()
136
+ ], CentralQElement.prototype, "position", 2);
137
+ __decorateClass([
138
+ (0, import_decorators.state)()
139
+ ], CentralQElement.prototype, "ahead", 2);
140
+ CentralQElement = __decorateClass([
141
+ (0, import_decorators.customElement)("central-q")
142
+ ], CentralQElement);
143
+ }
144
+ });
145
+
146
+ // src/react.ts
147
+ var react_exports = {};
148
+ __export(react_exports, {
149
+ useCentralQ: () => useCentralQ,
150
+ useQueue: () => useQueue
151
+ });
152
+ module.exports = __toCommonJS(react_exports);
153
+ var import_react = require("react");
154
+
155
+ // src/queue-client.ts
156
+ var CENTRALQ_DEFAULT_API_URL = "http://localhost:3001";
157
+ function normalizeApiUrl(apiUrl) {
158
+ const trimmed = apiUrl.replace(/\/+$/, "");
159
+ return trimmed.endsWith("/api") ? trimmed.slice(0, -4) : trimmed;
160
+ }
161
+ var QueueHttpClient = class {
162
+ constructor(apiUrl = CENTRALQ_DEFAULT_API_URL, apiKey, queueInitToken) {
163
+ this.apiUrl = apiUrl;
164
+ this.apiKey = apiKey;
165
+ this.queueInitToken = queueInitToken;
166
+ this.apiUrl = normalizeApiUrl(this.apiUrl);
167
+ }
168
+ get headers() {
169
+ const h = { "Content-Type": "application/json" };
170
+ if (this.apiKey) h["x-api-key"] = this.apiKey;
171
+ if (this.queueInitToken) h["x-queue-init-token"] = this.queueInitToken;
172
+ return h;
173
+ }
174
+ /**
175
+ * Intenta unir a un usuario a la cola de un evento.
176
+ * Llama a POST /api/queue/:eventId/join en el worker.
177
+ */
178
+ async joinQueue(eventId, userId) {
179
+ const res = await fetch(
180
+ `${this.apiUrl}/api/queue/${encodeURIComponent(eventId)}/join`,
181
+ {
182
+ method: "POST",
183
+ headers: this.headers,
184
+ body: JSON.stringify({ userId })
185
+ }
186
+ );
187
+ if (!res.ok) {
188
+ const body = await res.json().catch(() => ({}));
189
+ throw new Error(
190
+ body.message ?? `Error ${res.status} al conectar con el sistema de colas`
191
+ );
192
+ }
193
+ return res.json();
194
+ }
195
+ /**
196
+ * Envía un heartbeat para mantener vivo el slot (waiting o active).
197
+ * El SDK lo llama cada 10s automáticamente.
198
+ */
199
+ async sendHeartbeat(eventId, userId) {
200
+ await fetch(
201
+ `${this.apiUrl}/api/queue/${encodeURIComponent(eventId)}/heartbeat`,
202
+ {
203
+ method: "POST",
204
+ headers: this.headers,
205
+ body: JSON.stringify({ userId })
206
+ }
207
+ );
208
+ }
209
+ /**
210
+ * Libera el slot del usuario inmediatamente.
211
+ * Llamar al completar la compra o al salir voluntariamente.
212
+ */
213
+ async leaveQueue(eventId, userId) {
214
+ await fetch(
215
+ `${this.apiUrl}/api/queue/${encodeURIComponent(eventId)}/leave`,
216
+ {
217
+ method: "POST",
218
+ headers: this.headers,
219
+ body: JSON.stringify({ userId })
220
+ }
221
+ );
222
+ }
223
+ async issueQueueInitToken(eventId, userId) {
224
+ const res = await fetch(
225
+ `${this.apiUrl}/api/queue/init/${encodeURIComponent(eventId)}`,
226
+ {
227
+ method: "POST",
228
+ headers: this.headers,
229
+ body: JSON.stringify({ userId })
230
+ }
231
+ );
232
+ const body = await res.json().catch(() => ({}));
233
+ if (!res.ok) {
234
+ throw new Error(
235
+ body.message ?? `Error ${res.status} al inicializar sesi\xF3n de cola`
236
+ );
237
+ }
238
+ const token = body.queueInitToken ?? body.initToken;
239
+ if (!body.userId || !token || !body.expiresAt) {
240
+ throw new Error("Respuesta inv\xE1lida al inicializar sesi\xF3n de cola");
241
+ }
242
+ return {
243
+ userId: body.userId,
244
+ queueInitToken: token,
245
+ expiresAt: body.expiresAt
246
+ };
247
+ }
248
+ };
249
+
250
+ // src/centralq.ts
251
+ var CentralQ = class _CentralQ {
252
+ constructor(options) {
253
+ this.element = null;
254
+ this.destroyed = false;
255
+ // biome-ignore lint: allow explicit any for generic event map
256
+ this.listeners = /* @__PURE__ */ new Map();
257
+ this.client = new QueueHttpClient(
258
+ options.apiUrl,
259
+ options.apiKey,
260
+ options.queueInitToken
261
+ );
262
+ this.eventId = options.eventId;
263
+ this.pollInterval = options.pollInterval ?? 1e4;
264
+ this.container = options.container ?? document.body;
265
+ this.userId = this.resolveUserId(options.userId);
266
+ }
267
+ /**
268
+ * Inicializa CentralQ y comienza el flujo de cola automáticamente.
269
+ * Monta el overlay de UI y empieza a hacer polling.
270
+ */
271
+ static init(options) {
272
+ const instance = new _CentralQ(options);
273
+ instance.mount();
274
+ instance.checkQueue();
275
+ return instance;
276
+ }
277
+ // Event API
278
+ /** Suscribirse a un evento del ciclo de vida de la cola */
279
+ on(event, listener) {
280
+ if (!this.listeners.has(event)) {
281
+ this.listeners.set(event, /* @__PURE__ */ new Set());
282
+ }
283
+ this.listeners.get(event).add(listener);
284
+ return this;
285
+ }
286
+ /** Desuscribirse de un evento */
287
+ off(event, listener) {
288
+ this.listeners.get(event)?.delete(listener);
289
+ return this;
290
+ }
291
+ emit(event, detail) {
292
+ this.listeners.get(event)?.forEach((fn) => fn(detail));
293
+ }
294
+ // Métodos públicos
295
+ /**
296
+ * Libera el slot del usuario inmediatamente.
297
+ * Llamar cuando el usuario completa la compra.
298
+ */
299
+ leave() {
300
+ this.stopTimers();
301
+ this.updateUI("ACTIVE");
302
+ this.client.leaveQueue(this.eventId, this.userId).catch(() => {
303
+ });
304
+ }
305
+ /**
306
+ * Destruye la instancia: para todos los timers, desmonta el overlay,
307
+ * y libera el slot en el servidor.
308
+ */
309
+ destroy() {
310
+ this.destroyed = true;
311
+ this.stopTimers();
312
+ this.client.leaveQueue(this.eventId, this.userId).catch(() => {
313
+ });
314
+ this.unmount();
315
+ this.listeners.clear();
316
+ }
317
+ /** Retorna el userId resuelto (externo o autogenerado) */
318
+ getUserId() {
319
+ return this.userId;
320
+ }
321
+ // userId: genera y persiste automáticamente
322
+ resolveUserId(externalId) {
323
+ if (externalId) return externalId;
324
+ const key = `ctq_anon_${this.eventId}`;
325
+ try {
326
+ const stored = sessionStorage.getItem(key);
327
+ if (stored) return stored;
328
+ } catch {
329
+ }
330
+ const generated = `anon_${crypto.randomUUID()}`;
331
+ try {
332
+ sessionStorage.setItem(key, generated);
333
+ } catch {
334
+ }
335
+ return generated;
336
+ }
337
+ // UI: montar/desmontar el Web Component
338
+ mount() {
339
+ Promise.resolve().then(() => init_queue_element());
340
+ this.element = document.createElement("central-q");
341
+ this.element.onRetry = () => {
342
+ this.updateUI("LOADING");
343
+ this.checkQueue();
344
+ };
345
+ this.container.appendChild(this.element);
346
+ }
347
+ unmount() {
348
+ if (this.element) {
349
+ this.element.remove();
350
+ this.element = null;
351
+ }
352
+ }
353
+ updateUI(status, data) {
354
+ if (!this.element) return;
355
+ this.element.status = status;
356
+ if (data?.position !== void 0) this.element.position = data.position;
357
+ if (data?.ahead !== void 0) this.element.ahead = data.ahead;
358
+ }
359
+ // Timers
360
+ stopTimers() {
361
+ if (this.pollingTimer) {
362
+ clearInterval(this.pollingTimer);
363
+ this.pollingTimer = void 0;
364
+ }
365
+ if (this.heartbeatTimer) {
366
+ clearInterval(this.heartbeatTimer);
367
+ this.heartbeatTimer = void 0;
368
+ }
369
+ if (this.expiryTimer) {
370
+ clearTimeout(this.expiryTimer);
371
+ this.expiryTimer = void 0;
372
+ }
373
+ }
374
+ startHeartbeat() {
375
+ if (this.heartbeatTimer) return;
376
+ this.heartbeatTimer = setInterval(() => {
377
+ this.client.sendHeartbeat(this.eventId, this.userId).catch(() => {
378
+ });
379
+ }, this.pollInterval);
380
+ }
381
+ scheduleExpiry(expiresAt) {
382
+ if (this.expiryTimer) clearTimeout(this.expiryTimer);
383
+ const msLeft = expiresAt * 1e3 - Date.now();
384
+ if (msLeft <= 0) {
385
+ this.handleExpired();
386
+ return;
387
+ }
388
+ this.expiryTimer = setTimeout(() => this.handleExpired(), msLeft);
389
+ }
390
+ handleExpired() {
391
+ this.stopTimers();
392
+ this.updateUI("EXPIRED");
393
+ this.emit("expired", {
394
+ userId: this.userId,
395
+ eventId: this.eventId
396
+ });
397
+ }
398
+ // Core: polling + join
399
+ async checkQueue() {
400
+ if (this.destroyed) return;
401
+ try {
402
+ const res = await this.client.joinQueue(this.eventId, this.userId);
403
+ if (res.status === "ACTIVE") {
404
+ if (this.pollingTimer) {
405
+ clearInterval(this.pollingTimer);
406
+ this.pollingTimer = void 0;
407
+ }
408
+ this.startHeartbeat();
409
+ this.scheduleExpiry(res.expiresAt);
410
+ this.updateUI("ACTIVE");
411
+ this.emit("passed", {
412
+ token: res.token,
413
+ expiresAt: res.expiresAt,
414
+ userId: this.userId,
415
+ eventId: this.eventId
416
+ });
417
+ } else if (res.status === "WAITING") {
418
+ this.updateUI("WAITING", {
419
+ position: res.position,
420
+ ahead: res.ahead
421
+ });
422
+ this.emit("position", {
423
+ position: res.position,
424
+ ahead: res.ahead,
425
+ userId: this.userId,
426
+ eventId: this.eventId
427
+ });
428
+ if (!this.pollingTimer) {
429
+ this.pollingTimer = setInterval(
430
+ () => this.checkQueue(),
431
+ this.pollInterval
432
+ );
433
+ }
434
+ }
435
+ } catch {
436
+ this.updateUI("ERROR");
437
+ this.stopTimers();
438
+ this.emit("error", {
439
+ userId: this.userId,
440
+ eventId: this.eventId
441
+ });
442
+ }
443
+ }
444
+ };
445
+
446
+ // src/react.ts
447
+ function useCentralQ(options) {
448
+ const [token, setToken] = (0, import_react.useState)(null);
449
+ const [position, setPosition] = (0, import_react.useState)(null);
450
+ const [ahead, setAhead] = (0, import_react.useState)(null);
451
+ const [expired, setExpired] = (0, import_react.useState)(false);
452
+ const [error, setError] = (0, import_react.useState)(false);
453
+ const instanceRef = (0, import_react.useRef)(null);
454
+ const leave = (0, import_react.useCallback)(() => {
455
+ instanceRef.current?.leave();
456
+ setToken(null);
457
+ setPosition(null);
458
+ setAhead(null);
459
+ }, []);
460
+ (0, import_react.useEffect)(() => {
461
+ const queue = CentralQ.init(options);
462
+ queue.on("passed", (detail) => {
463
+ setToken(detail.token);
464
+ setPosition(null);
465
+ setAhead(null);
466
+ setExpired(false);
467
+ setError(false);
468
+ });
469
+ queue.on("position", (detail) => {
470
+ setPosition(detail.position);
471
+ setAhead(detail.ahead);
472
+ });
473
+ queue.on("expired", () => {
474
+ setToken(null);
475
+ setExpired(true);
476
+ });
477
+ queue.on("error", () => {
478
+ setError(true);
479
+ });
480
+ instanceRef.current = queue;
481
+ return () => {
482
+ queue.destroy();
483
+ instanceRef.current = null;
484
+ };
485
+ }, [options.apiUrl, options.apiKey, options.eventId]);
486
+ return {
487
+ token,
488
+ position,
489
+ ahead,
490
+ expired,
491
+ error,
492
+ leave,
493
+ instance: instanceRef.current
494
+ };
495
+ }
496
+ function useQueue(options) {
497
+ const [status, setStatus] = (0, import_react.useState)("idle");
498
+ const [data, setData] = (0, import_react.useState)({
499
+ token: null,
500
+ position: null,
501
+ ahead: null,
502
+ expiresAt: null
503
+ });
504
+ const [error, setError] = (0, import_react.useState)(null);
505
+ const instanceRef = (0, import_react.useRef)(null);
506
+ const reset = (0, import_react.useCallback)(() => {
507
+ setStatus("idle");
508
+ setError(null);
509
+ setData({ token: null, position: null, ahead: null, expiresAt: null });
510
+ }, []);
511
+ const leave = (0, import_react.useCallback)(() => {
512
+ instanceRef.current?.leave();
513
+ setData({ token: null, position: null, ahead: null, expiresAt: null });
514
+ setStatus("idle");
515
+ }, []);
516
+ (0, import_react.useEffect)(() => {
517
+ let queue = null;
518
+ let cancelled = false;
519
+ if (options.enabled === false) {
520
+ instanceRef.current?.destroy();
521
+ instanceRef.current = null;
522
+ reset();
523
+ return;
524
+ }
525
+ setStatus("joining");
526
+ setError(null);
527
+ const init = async () => {
528
+ try {
529
+ let userId = options.userId;
530
+ let queueInitToken = options.queueInitToken;
531
+ if (!userId || !queueInitToken) {
532
+ if (options.autoInitToken === false) {
533
+ throw new Error(
534
+ "Falta queueInitToken/userId y initEndpoint est\xE1 deshabilitado"
535
+ );
536
+ }
537
+ const init2 = await options.client.issueQueueInitToken(
538
+ options.eventId,
539
+ userId
540
+ );
541
+ userId = init2.userId;
542
+ queueInitToken = init2.queueInitToken;
543
+ }
544
+ if (cancelled) return;
545
+ queue = options.client.createQueue({
546
+ eventId: options.eventId,
547
+ userId,
548
+ queueInitToken,
549
+ pollInterval: options.pollInterval,
550
+ container: options.container
551
+ });
552
+ queue.on("passed", (detail) => {
553
+ setData({
554
+ token: detail.token,
555
+ position: null,
556
+ ahead: null,
557
+ expiresAt: detail.expiresAt
558
+ });
559
+ setStatus("passed");
560
+ setError(null);
561
+ });
562
+ queue.on("position", (detail) => {
563
+ setData((prev) => ({
564
+ ...prev,
565
+ token: null,
566
+ position: detail.position,
567
+ ahead: detail.ahead
568
+ }));
569
+ setStatus("waiting");
570
+ });
571
+ queue.on("expired", () => {
572
+ setData({
573
+ token: null,
574
+ position: null,
575
+ ahead: null,
576
+ expiresAt: null
577
+ });
578
+ setStatus("expired");
579
+ });
580
+ queue.on("error", () => {
581
+ setStatus("error");
582
+ setError(new Error("Queue connection error"));
583
+ });
584
+ instanceRef.current = queue;
585
+ } catch (err) {
586
+ if (cancelled) return;
587
+ setStatus("error");
588
+ setError(
589
+ err instanceof Error ? err : new Error("Queue connection error")
590
+ );
591
+ }
592
+ };
593
+ init();
594
+ return () => {
595
+ cancelled = true;
596
+ queue?.destroy();
597
+ instanceRef.current = null;
598
+ };
599
+ }, [
600
+ options.client,
601
+ options.enabled,
602
+ options.eventId,
603
+ options.userId,
604
+ options.queueInitToken,
605
+ options.autoInitToken,
606
+ options.pollInterval,
607
+ options.container,
608
+ reset
609
+ ]);
610
+ return {
611
+ status,
612
+ data,
613
+ isPending: status === "idle" || status === "joining",
614
+ error,
615
+ leave,
616
+ reset,
617
+ instance: instanceRef.current
618
+ };
619
+ }
620
+ // Annotate the CommonJS export names for ESM import in node:
621
+ 0 && (module.exports = {
622
+ useCentralQ,
623
+ useQueue
624
+ });