@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/svelte.js ADDED
@@ -0,0 +1,628 @@
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/svelte.ts
147
+ var svelte_exports = {};
148
+ __export(svelte_exports, {
149
+ createCentralQ: () => createCentralQ,
150
+ useQueue: () => useQueue
151
+ });
152
+ module.exports = __toCommonJS(svelte_exports);
153
+ var import_store = require("svelte/store");
154
+ var import_svelte = require("svelte");
155
+
156
+ // src/queue-client.ts
157
+ var CENTRALQ_DEFAULT_API_URL = "http://localhost:3001";
158
+ function normalizeApiUrl(apiUrl) {
159
+ const trimmed = apiUrl.replace(/\/+$/, "");
160
+ return trimmed.endsWith("/api") ? trimmed.slice(0, -4) : trimmed;
161
+ }
162
+ var QueueHttpClient = class {
163
+ constructor(apiUrl = CENTRALQ_DEFAULT_API_URL, apiKey, queueInitToken) {
164
+ this.apiUrl = apiUrl;
165
+ this.apiKey = apiKey;
166
+ this.queueInitToken = queueInitToken;
167
+ this.apiUrl = normalizeApiUrl(this.apiUrl);
168
+ }
169
+ get headers() {
170
+ const h = { "Content-Type": "application/json" };
171
+ if (this.apiKey) h["x-api-key"] = this.apiKey;
172
+ if (this.queueInitToken) h["x-queue-init-token"] = this.queueInitToken;
173
+ return h;
174
+ }
175
+ /**
176
+ * Intenta unir a un usuario a la cola de un evento.
177
+ * Llama a POST /api/queue/:eventId/join en el worker.
178
+ */
179
+ async joinQueue(eventId, userId) {
180
+ const res = await fetch(
181
+ `${this.apiUrl}/api/queue/${encodeURIComponent(eventId)}/join`,
182
+ {
183
+ method: "POST",
184
+ headers: this.headers,
185
+ body: JSON.stringify({ userId })
186
+ }
187
+ );
188
+ if (!res.ok) {
189
+ const body = await res.json().catch(() => ({}));
190
+ throw new Error(
191
+ body.message ?? `Error ${res.status} al conectar con el sistema de colas`
192
+ );
193
+ }
194
+ return res.json();
195
+ }
196
+ /**
197
+ * Envía un heartbeat para mantener vivo el slot (waiting o active).
198
+ * El SDK lo llama cada 10s automáticamente.
199
+ */
200
+ async sendHeartbeat(eventId, userId) {
201
+ await fetch(
202
+ `${this.apiUrl}/api/queue/${encodeURIComponent(eventId)}/heartbeat`,
203
+ {
204
+ method: "POST",
205
+ headers: this.headers,
206
+ body: JSON.stringify({ userId })
207
+ }
208
+ );
209
+ }
210
+ /**
211
+ * Libera el slot del usuario inmediatamente.
212
+ * Llamar al completar la compra o al salir voluntariamente.
213
+ */
214
+ async leaveQueue(eventId, userId) {
215
+ await fetch(
216
+ `${this.apiUrl}/api/queue/${encodeURIComponent(eventId)}/leave`,
217
+ {
218
+ method: "POST",
219
+ headers: this.headers,
220
+ body: JSON.stringify({ userId })
221
+ }
222
+ );
223
+ }
224
+ async issueQueueInitToken(eventId, userId) {
225
+ const res = await fetch(
226
+ `${this.apiUrl}/api/queue/init/${encodeURIComponent(eventId)}`,
227
+ {
228
+ method: "POST",
229
+ headers: this.headers,
230
+ body: JSON.stringify({ userId })
231
+ }
232
+ );
233
+ const body = await res.json().catch(() => ({}));
234
+ if (!res.ok) {
235
+ throw new Error(
236
+ body.message ?? `Error ${res.status} al inicializar sesi\xF3n de cola`
237
+ );
238
+ }
239
+ const token = body.queueInitToken ?? body.initToken;
240
+ if (!body.userId || !token || !body.expiresAt) {
241
+ throw new Error("Respuesta inv\xE1lida al inicializar sesi\xF3n de cola");
242
+ }
243
+ return {
244
+ userId: body.userId,
245
+ queueInitToken: token,
246
+ expiresAt: body.expiresAt
247
+ };
248
+ }
249
+ };
250
+
251
+ // src/centralq.ts
252
+ var CentralQ = class _CentralQ {
253
+ constructor(options) {
254
+ this.element = null;
255
+ this.destroyed = false;
256
+ // biome-ignore lint: allow explicit any for generic event map
257
+ this.listeners = /* @__PURE__ */ new Map();
258
+ this.client = new QueueHttpClient(
259
+ options.apiUrl,
260
+ options.apiKey,
261
+ options.queueInitToken
262
+ );
263
+ this.eventId = options.eventId;
264
+ this.pollInterval = options.pollInterval ?? 1e4;
265
+ this.container = options.container ?? document.body;
266
+ this.userId = this.resolveUserId(options.userId);
267
+ }
268
+ /**
269
+ * Inicializa CentralQ y comienza el flujo de cola automáticamente.
270
+ * Monta el overlay de UI y empieza a hacer polling.
271
+ */
272
+ static init(options) {
273
+ const instance = new _CentralQ(options);
274
+ instance.mount();
275
+ instance.checkQueue();
276
+ return instance;
277
+ }
278
+ // Event API
279
+ /** Suscribirse a un evento del ciclo de vida de la cola */
280
+ on(event, listener) {
281
+ if (!this.listeners.has(event)) {
282
+ this.listeners.set(event, /* @__PURE__ */ new Set());
283
+ }
284
+ this.listeners.get(event).add(listener);
285
+ return this;
286
+ }
287
+ /** Desuscribirse de un evento */
288
+ off(event, listener) {
289
+ this.listeners.get(event)?.delete(listener);
290
+ return this;
291
+ }
292
+ emit(event, detail) {
293
+ this.listeners.get(event)?.forEach((fn) => fn(detail));
294
+ }
295
+ // Métodos públicos
296
+ /**
297
+ * Libera el slot del usuario inmediatamente.
298
+ * Llamar cuando el usuario completa la compra.
299
+ */
300
+ leave() {
301
+ this.stopTimers();
302
+ this.updateUI("ACTIVE");
303
+ this.client.leaveQueue(this.eventId, this.userId).catch(() => {
304
+ });
305
+ }
306
+ /**
307
+ * Destruye la instancia: para todos los timers, desmonta el overlay,
308
+ * y libera el slot en el servidor.
309
+ */
310
+ destroy() {
311
+ this.destroyed = true;
312
+ this.stopTimers();
313
+ this.client.leaveQueue(this.eventId, this.userId).catch(() => {
314
+ });
315
+ this.unmount();
316
+ this.listeners.clear();
317
+ }
318
+ /** Retorna el userId resuelto (externo o autogenerado) */
319
+ getUserId() {
320
+ return this.userId;
321
+ }
322
+ // userId: genera y persiste automáticamente
323
+ resolveUserId(externalId) {
324
+ if (externalId) return externalId;
325
+ const key = `ctq_anon_${this.eventId}`;
326
+ try {
327
+ const stored = sessionStorage.getItem(key);
328
+ if (stored) return stored;
329
+ } catch {
330
+ }
331
+ const generated = `anon_${crypto.randomUUID()}`;
332
+ try {
333
+ sessionStorage.setItem(key, generated);
334
+ } catch {
335
+ }
336
+ return generated;
337
+ }
338
+ // UI: montar/desmontar el Web Component
339
+ mount() {
340
+ Promise.resolve().then(() => init_queue_element());
341
+ this.element = document.createElement("central-q");
342
+ this.element.onRetry = () => {
343
+ this.updateUI("LOADING");
344
+ this.checkQueue();
345
+ };
346
+ this.container.appendChild(this.element);
347
+ }
348
+ unmount() {
349
+ if (this.element) {
350
+ this.element.remove();
351
+ this.element = null;
352
+ }
353
+ }
354
+ updateUI(status, data) {
355
+ if (!this.element) return;
356
+ this.element.status = status;
357
+ if (data?.position !== void 0) this.element.position = data.position;
358
+ if (data?.ahead !== void 0) this.element.ahead = data.ahead;
359
+ }
360
+ // Timers
361
+ stopTimers() {
362
+ if (this.pollingTimer) {
363
+ clearInterval(this.pollingTimer);
364
+ this.pollingTimer = void 0;
365
+ }
366
+ if (this.heartbeatTimer) {
367
+ clearInterval(this.heartbeatTimer);
368
+ this.heartbeatTimer = void 0;
369
+ }
370
+ if (this.expiryTimer) {
371
+ clearTimeout(this.expiryTimer);
372
+ this.expiryTimer = void 0;
373
+ }
374
+ }
375
+ startHeartbeat() {
376
+ if (this.heartbeatTimer) return;
377
+ this.heartbeatTimer = setInterval(() => {
378
+ this.client.sendHeartbeat(this.eventId, this.userId).catch(() => {
379
+ });
380
+ }, this.pollInterval);
381
+ }
382
+ scheduleExpiry(expiresAt) {
383
+ if (this.expiryTimer) clearTimeout(this.expiryTimer);
384
+ const msLeft = expiresAt * 1e3 - Date.now();
385
+ if (msLeft <= 0) {
386
+ this.handleExpired();
387
+ return;
388
+ }
389
+ this.expiryTimer = setTimeout(() => this.handleExpired(), msLeft);
390
+ }
391
+ handleExpired() {
392
+ this.stopTimers();
393
+ this.updateUI("EXPIRED");
394
+ this.emit("expired", {
395
+ userId: this.userId,
396
+ eventId: this.eventId
397
+ });
398
+ }
399
+ // Core: polling + join
400
+ async checkQueue() {
401
+ if (this.destroyed) return;
402
+ try {
403
+ const res = await this.client.joinQueue(this.eventId, this.userId);
404
+ if (res.status === "ACTIVE") {
405
+ if (this.pollingTimer) {
406
+ clearInterval(this.pollingTimer);
407
+ this.pollingTimer = void 0;
408
+ }
409
+ this.startHeartbeat();
410
+ this.scheduleExpiry(res.expiresAt);
411
+ this.updateUI("ACTIVE");
412
+ this.emit("passed", {
413
+ token: res.token,
414
+ expiresAt: res.expiresAt,
415
+ userId: this.userId,
416
+ eventId: this.eventId
417
+ });
418
+ } else if (res.status === "WAITING") {
419
+ this.updateUI("WAITING", {
420
+ position: res.position,
421
+ ahead: res.ahead
422
+ });
423
+ this.emit("position", {
424
+ position: res.position,
425
+ ahead: res.ahead,
426
+ userId: this.userId,
427
+ eventId: this.eventId
428
+ });
429
+ if (!this.pollingTimer) {
430
+ this.pollingTimer = setInterval(
431
+ () => this.checkQueue(),
432
+ this.pollInterval
433
+ );
434
+ }
435
+ }
436
+ } catch {
437
+ this.updateUI("ERROR");
438
+ this.stopTimers();
439
+ this.emit("error", {
440
+ userId: this.userId,
441
+ eventId: this.eventId
442
+ });
443
+ }
444
+ }
445
+ };
446
+
447
+ // src/svelte.ts
448
+ function createCentralQ(options) {
449
+ const _token = (0, import_store.writable)(null);
450
+ const _position = (0, import_store.writable)(null);
451
+ const _ahead = (0, import_store.writable)(null);
452
+ const _expired = (0, import_store.writable)(false);
453
+ const _error = (0, import_store.writable)(false);
454
+ let queue = null;
455
+ (0, import_svelte.onMount)(() => {
456
+ queue = CentralQ.init(options);
457
+ queue.on("passed", (detail) => {
458
+ _token.set(detail.token);
459
+ _position.set(null);
460
+ _ahead.set(null);
461
+ _expired.set(false);
462
+ _error.set(false);
463
+ });
464
+ queue.on("position", (detail) => {
465
+ _position.set(detail.position);
466
+ _ahead.set(detail.ahead);
467
+ });
468
+ queue.on("expired", () => {
469
+ _token.set(null);
470
+ _expired.set(true);
471
+ });
472
+ queue.on("error", () => {
473
+ _error.set(true);
474
+ });
475
+ return () => {
476
+ queue?.destroy();
477
+ queue = null;
478
+ };
479
+ });
480
+ return {
481
+ token: (0, import_store.readonly)(_token),
482
+ position: (0, import_store.readonly)(_position),
483
+ ahead: (0, import_store.readonly)(_ahead),
484
+ expired: (0, import_store.readonly)(_expired),
485
+ error: (0, import_store.readonly)(_error),
486
+ leave: () => {
487
+ queue?.leave();
488
+ _token.set(null);
489
+ _position.set(null);
490
+ _ahead.set(null);
491
+ }
492
+ };
493
+ }
494
+ function useQueue(optionsInput) {
495
+ const status = (0, import_store.writable)("idle");
496
+ const data = (0, import_store.writable)({
497
+ token: null,
498
+ position: null,
499
+ ahead: null,
500
+ expiresAt: null
501
+ });
502
+ const error = (0, import_store.writable)(null);
503
+ const isPending = (0, import_store.writable)(true);
504
+ const instance = (0, import_store.writable)(null);
505
+ const token = (0, import_store.derived)(data, ($data) => $data.token);
506
+ const position = (0, import_store.derived)(data, ($data) => $data.position);
507
+ const ahead = (0, import_store.derived)(data, ($data) => $data.ahead);
508
+ const expiresAt = (0, import_store.derived)(data, ($data) => $data.expiresAt);
509
+ const reset = () => {
510
+ status.set("idle");
511
+ error.set(null);
512
+ data.set({ token: null, position: null, ahead: null, expiresAt: null });
513
+ isPending.set(true);
514
+ };
515
+ const leave = () => {
516
+ (0, import_store.get)(instance)?.leave();
517
+ status.set("idle");
518
+ data.set({ token: null, position: null, ahead: null, expiresAt: null });
519
+ isPending.set(true);
520
+ };
521
+ (0, import_svelte.onMount)(() => {
522
+ let queue = null;
523
+ let cancelled = false;
524
+ const options = typeof optionsInput === "function" ? optionsInput() : optionsInput;
525
+ if (options.enabled === false) {
526
+ reset();
527
+ return;
528
+ }
529
+ status.set("joining");
530
+ error.set(null);
531
+ isPending.set(true);
532
+ const init = async () => {
533
+ try {
534
+ let userId = options.userId;
535
+ let queueInitToken = options.queueInitToken;
536
+ if (!userId || !queueInitToken) {
537
+ if (options.autoInitToken === false) {
538
+ throw new Error(
539
+ "Falta queueInitToken/userId y initEndpoint est\xE1 deshabilitado"
540
+ );
541
+ }
542
+ const init2 = await options.client.issueQueueInitToken(
543
+ options.eventId,
544
+ userId
545
+ );
546
+ userId = init2.userId;
547
+ queueInitToken = init2.queueInitToken;
548
+ }
549
+ if (cancelled) return;
550
+ queue = options.client.createQueue({
551
+ eventId: options.eventId,
552
+ userId,
553
+ queueInitToken,
554
+ pollInterval: options.pollInterval,
555
+ container: options.container
556
+ });
557
+ queue.on("passed", (detail) => {
558
+ data.set({
559
+ token: detail.token,
560
+ position: null,
561
+ ahead: null,
562
+ expiresAt: detail.expiresAt
563
+ });
564
+ status.set("passed");
565
+ error.set(null);
566
+ isPending.set(false);
567
+ });
568
+ queue.on("position", (detail) => {
569
+ data.update((prev) => ({
570
+ ...prev,
571
+ token: null,
572
+ position: detail.position,
573
+ ahead: detail.ahead
574
+ }));
575
+ status.set("waiting");
576
+ isPending.set(false);
577
+ });
578
+ queue.on("expired", () => {
579
+ data.set({
580
+ token: null,
581
+ position: null,
582
+ ahead: null,
583
+ expiresAt: null
584
+ });
585
+ status.set("expired");
586
+ isPending.set(false);
587
+ });
588
+ queue.on("error", () => {
589
+ status.set("error");
590
+ error.set(new Error("Queue connection error"));
591
+ isPending.set(false);
592
+ });
593
+ instance.set(queue);
594
+ } catch (err) {
595
+ if (cancelled) return;
596
+ status.set("error");
597
+ error.set(
598
+ err instanceof Error ? err : new Error("Queue connection error")
599
+ );
600
+ isPending.set(false);
601
+ }
602
+ };
603
+ init();
604
+ return () => {
605
+ cancelled = true;
606
+ queue?.destroy();
607
+ instance.set(null);
608
+ };
609
+ });
610
+ return {
611
+ status: (0, import_store.readonly)(status),
612
+ token,
613
+ position,
614
+ ahead,
615
+ expiresAt,
616
+ data: (0, import_store.readonly)(data),
617
+ isPending: (0, import_store.readonly)(isPending),
618
+ error: (0, import_store.readonly)(error),
619
+ leave,
620
+ reset,
621
+ instance: (0, import_store.readonly)(instance)
622
+ };
623
+ }
624
+ // Annotate the CommonJS export names for ESM import in node:
625
+ 0 && (module.exports = {
626
+ createCentralQ,
627
+ useQueue
628
+ });