@elliemae/pui-websocket-so 2.0.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.
Files changed (36) hide show
  1. package/dist/cjs/index.html +758 -0
  2. package/dist/cjs/index.js +24 -0
  3. package/dist/cjs/messageRouter.js +111 -0
  4. package/dist/cjs/package.json +7 -0
  5. package/dist/cjs/subscriptionManager.js +224 -0
  6. package/dist/cjs/types.js +16 -0
  7. package/dist/cjs/websocketSO.js +338 -0
  8. package/dist/esm/index.html +758 -0
  9. package/dist/esm/index.js +4 -0
  10. package/dist/esm/messageRouter.js +91 -0
  11. package/dist/esm/package.json +7 -0
  12. package/dist/esm/subscriptionManager.js +204 -0
  13. package/dist/esm/types.js +0 -0
  14. package/dist/esm/websocketSO.js +318 -0
  15. package/dist/public/guest.html +523 -0
  16. package/dist/public/index.html +1 -0
  17. package/dist/public/js/emuiWebsocketSo.cc1f5b5e1d095fc3a34e.js +3 -0
  18. package/dist/public/js/emuiWebsocketSo.cc1f5b5e1d095fc3a34e.js.br +0 -0
  19. package/dist/public/js/emuiWebsocketSo.cc1f5b5e1d095fc3a34e.js.gz +0 -0
  20. package/dist/public/js/emuiWebsocketSo.cc1f5b5e1d095fc3a34e.js.map +1 -0
  21. package/dist/types/lib/index.d.ts +2 -0
  22. package/dist/types/lib/messageRouter.d.ts +30 -0
  23. package/dist/types/lib/subscriptionManager.d.ts +101 -0
  24. package/dist/types/lib/tests/messageRouter.test.d.ts +1 -0
  25. package/dist/types/lib/tests/subscriptionManager.test.d.ts +1 -0
  26. package/dist/types/lib/tests/websocketSO.test.d.ts +1 -0
  27. package/dist/types/lib/types.d.ts +118 -0
  28. package/dist/types/lib/websocketSO.d.ts +56 -0
  29. package/dist/types/tsconfig.tsbuildinfo +1 -0
  30. package/dist/umd/guest.html +523 -0
  31. package/dist/umd/index.html +1 -0
  32. package/dist/umd/index.js +3 -0
  33. package/dist/umd/index.js.br +0 -0
  34. package/dist/umd/index.js.gz +0 -0
  35. package/dist/umd/index.js.map +1 -0
  36. package/package.json +69 -0
@@ -0,0 +1,523 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Guest Simulator</title>
7
+ <script src="https://cdn.tailwindcss.com?plugins=forms"></script>
8
+
9
+ <style>
10
+ /* ── JSON syntax colours ───────────────────────────── */
11
+ .jk {
12
+ color: #c084fc;
13
+ }
14
+ .js {
15
+ color: #4ade80;
16
+ }
17
+ .jn {
18
+ color: #fb923c;
19
+ }
20
+ .jb {
21
+ color: #60a5fa;
22
+ }
23
+ .jz {
24
+ color: #6b7280;
25
+ }
26
+
27
+ /* ── Scrollable panels ─────────────────────────────── */
28
+ .scroll-panel {
29
+ overflow-y: auto;
30
+ }
31
+ pre {
32
+ white-space: pre-wrap;
33
+ word-break: break-all;
34
+ font-size: 0.72rem;
35
+ }
36
+
37
+ /* ── Event log borders ─────────────────────────────── */
38
+ .log-event {
39
+ border-left: 3px solid #818cf8;
40
+ }
41
+ .log-sub {
42
+ border-left: 3px solid #34d399;
43
+ }
44
+ .log-unsub {
45
+ border-left: 3px solid #94a3b8;
46
+ }
47
+ .log-error {
48
+ border-left: 3px solid #f87171;
49
+ }
50
+ .log-info {
51
+ border-left: 3px solid #6366f1;
52
+ }
53
+ </style>
54
+ </head>
55
+
56
+ <body
57
+ class="bg-white text-sm text-gray-800 font-sans flex flex-col"
58
+ style="min-height: 100vh"
59
+ >
60
+ <!-- ════════════════ MINI HEADER ════════════════════ -->
61
+ <div
62
+ class="flex items-center justify-between px-3 py-1.5 bg-indigo-50 border-b border-indigo-100 shrink-0"
63
+ >
64
+ <span class="text-xs font-semibold text-indigo-700">Guest Simulator</span>
65
+ <span
66
+ id="connBadge"
67
+ class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium bg-gray-100 text-gray-500"
68
+ >
69
+ ● Connecting…
70
+ </span>
71
+ </div>
72
+
73
+ <!-- ════════════════ CONNECTING STATE ══════════════ -->
74
+ <div
75
+ id="connectingState"
76
+ class="flex flex-col items-center justify-center flex-1 py-8 text-gray-400 gap-2"
77
+ >
78
+ <svg
79
+ class="w-8 h-8 animate-spin text-indigo-400"
80
+ fill="none"
81
+ viewBox="0 0 24 24"
82
+ >
83
+ <circle
84
+ class="opacity-25"
85
+ cx="12"
86
+ cy="12"
87
+ r="10"
88
+ stroke="currentColor"
89
+ stroke-width="4"
90
+ />
91
+ <path
92
+ class="opacity-75"
93
+ fill="currentColor"
94
+ d="M4 12a8 8 0 018-8v8H4z"
95
+ />
96
+ </svg>
97
+ <p class="text-xs">Connecting to SSF Host…</p>
98
+ </div>
99
+
100
+ <!-- ════════════════ MAIN UI (hidden until connected) ══════════════ -->
101
+ <div id="mainUI" class="hidden flex-1 flex flex-col overflow-hidden">
102
+ <!-- ── Subscribe Form ── -->
103
+ <div class="px-3 py-2.5 border-b border-gray-100 bg-white">
104
+ <div class="flex items-center justify-between mb-2">
105
+ <h3
106
+ class="text-xs font-semibold text-gray-500 uppercase tracking-widest"
107
+ >
108
+ Subscribe
109
+ </h3>
110
+ <div class="flex gap-1 flex-wrap justify-end">
111
+ <button
112
+ data-tpl="subscribeResourceId"
113
+ class="tpl-btn text-xs px-2 py-0.5 rounded bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 transition"
114
+ >
115
+ serviceOrder / resourceId
116
+ </button>
117
+ <button
118
+ data-tpl="subscribeFilter"
119
+ class="tpl-btn text-xs px-2 py-0.5 rounded bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 transition"
120
+ >
121
+ serviceOrder / filter
122
+ </button>
123
+ <button
124
+ data-tpl="subscribeLoan"
125
+ class="tpl-btn text-xs px-2 py-0.5 rounded bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 transition"
126
+ >
127
+ loan / resourceId
128
+ </button>
129
+ </div>
130
+ </div>
131
+
132
+ <textarea
133
+ id="subscribeInput"
134
+ rows="5"
135
+ spellcheck="false"
136
+ placeholder='{ "resource": "serviceOrder", "resourceId": "..." }'
137
+ class="block w-full rounded border-gray-200 text-xs font-mono bg-gray-950 text-green-300 p-2 focus:ring-indigo-500 focus:border-indigo-500 resize-none"
138
+ ></textarea>
139
+
140
+ <div class="flex items-center gap-2 mt-2">
141
+ <button
142
+ id="btnSubscribe"
143
+ class="rounded bg-indigo-600 text-white text-xs font-medium px-3 py-1.5 hover:bg-indigo-700 transition"
144
+ >
145
+ Subscribe
146
+ </button>
147
+ <p id="subError" class="hidden text-xs text-red-600"></p>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- ── Active Subscriptions ── -->
152
+ <div id="subsSection" class="hidden px-3 py-2 border-b border-gray-100">
153
+ <h3
154
+ class="text-xs font-semibold text-gray-500 uppercase tracking-widest mb-2"
155
+ >
156
+ Active Subscriptions
157
+ </h3>
158
+ <div id="subsList" class="space-y-1"></div>
159
+ </div>
160
+
161
+ <!-- ── Event Log ── -->
162
+ <div class="flex flex-col flex-1 overflow-hidden">
163
+ <div
164
+ class="flex items-center justify-between px-3 py-1.5 border-b border-gray-100 bg-white shrink-0"
165
+ >
166
+ <h3
167
+ class="text-xs font-semibold text-gray-500 uppercase tracking-widest"
168
+ >
169
+ Events Received
170
+ </h3>
171
+ <button
172
+ id="btnClearEvents"
173
+ class="text-xs text-gray-400 hover:text-red-500 transition"
174
+ >
175
+ Clear
176
+ </button>
177
+ </div>
178
+ <div
179
+ id="eventsLog"
180
+ class="scroll-panel flex-1 p-2 bg-gray-950 font-mono"
181
+ >
182
+ <p
183
+ id="eventsPlaceholder"
184
+ class="text-gray-600 italic text-xs text-center py-4"
185
+ >
186
+ Subscribe to start receiving events.
187
+ </p>
188
+ </div>
189
+ </div>
190
+ </div>
191
+
192
+ <!-- ════════════════ DEPENDENCIES ════════════════════
193
+ Load order matters: pui-diagnostics MUST precede ssf-guest
194
+ because ssf-guest reads window.emuiDiagnostics at parse time.
195
+ ═══════════════════════════════════════════════════════ -->
196
+ <script src="https://assets.ellieservices.com/pui-diagnostics@3.2.0/umd/index.js"></script>
197
+ <script src="https://cdn.mortgagetech.ice.com/ssf-guest"></script>
198
+
199
+ <script>
200
+ /* ══════════════════════════════════════════════════════════════
201
+ Guest Simulator — connects to the SSF Host in parent window,
202
+ retrieves the 'websocket' scripting object proxy, and provides
203
+ UI for subscribe / unsubscribe / live event viewing.
204
+ ══════════════════════════════════════════════════════════════ */
205
+
206
+ /* ── Sample IDs (matching PDF Phase II spec) ──────────────── */
207
+ const SAMPLE_RESOURCE_ID = '1e65313c-b940-47a8-a65f-a0b6f7ca83f3';
208
+ const SAMPLE_LOAN_ID = '7e46313c-b940-47a8-a65f-a0b6f7ca83f4';
209
+
210
+ /* ── Subscribe option templates ───────────────────────────── */
211
+ const TEMPLATES = {
212
+ subscribeResourceId: () => ({
213
+ resource: 'serviceOrder',
214
+ resourceId: SAMPLE_RESOURCE_ID,
215
+ }),
216
+ subscribeFilter: () => ({
217
+ resource: 'serviceOrder',
218
+ filter: { loanId: [SAMPLE_LOAN_ID] },
219
+ }),
220
+ subscribeLoan: () => ({
221
+ resource: 'loan',
222
+ resourceId: SAMPLE_RESOURCE_ID,
223
+ }),
224
+ };
225
+
226
+ /* ── State ────────────────────────────────────────────────── */
227
+ let websocketProxy = null;
228
+ /** @type {Map<string, { resource: string, resourceId?: string, filter?: object }>} */
229
+ const activeSubs = new Map(); // subscriptionId → options
230
+
231
+ /* ── UI refs ──────────────────────────────────────────────── */
232
+ const $badge = document.getElementById('connBadge');
233
+ const $connecting = document.getElementById('connectingState');
234
+ const $mainUI = document.getElementById('mainUI');
235
+ const $subInput = document.getElementById('subscribeInput');
236
+ const $subError = document.getElementById('subError');
237
+ const $btnSubscribe = document.getElementById('btnSubscribe');
238
+ const $subsSection = document.getElementById('subsSection');
239
+ const $subsList = document.getElementById('subsList');
240
+ const $eventsLog = document.getElementById('eventsLog');
241
+ const $clearBtn = document.getElementById('btnClearEvents');
242
+
243
+ /* ── Logger ───────────────────────────────────────────────── */
244
+ function buildConsoleLogger(tag) {
245
+ const p = `[${tag}]`;
246
+ return {
247
+ debug: (m, ...a) => console.debug(p, m, ...a),
248
+ info: (m, ...a) => console.info(p, m, ...a),
249
+ warn: (m, ...a) => console.warn(p, m, ...a),
250
+ error: (m, ...a) => console.error(p, m, ...a),
251
+ audit: (m, ...a) => console.info(p, '[AUDIT]', m, ...a),
252
+ setLogLevel: () => {},
253
+ getLogLevel: () => 'debug',
254
+ };
255
+ }
256
+
257
+ /* ── Notify host page (for its log panel) ─────────────────── */
258
+ function notifyHost(type, payload) {
259
+ try {
260
+ window.parent.postMessage(
261
+ { __soPlayground: true, type, payload },
262
+ '*',
263
+ );
264
+ } catch {
265
+ /* cross-origin guard (shouldn't happen in playground) */
266
+ }
267
+ }
268
+
269
+ /* ── Badge / state ────────────────────────────────────────── */
270
+ function setConnected() {
271
+ $badge.className =
272
+ 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium bg-green-100 text-green-700';
273
+ $badge.textContent = '● Connected';
274
+ $connecting.classList.add('hidden');
275
+ $mainUI.classList.remove('hidden');
276
+ }
277
+ function setError(msg) {
278
+ $badge.className =
279
+ 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium bg-red-100 text-red-700';
280
+ $badge.textContent = '● Error';
281
+ $connecting.innerHTML = `<p class="text-xs text-red-500 px-4 text-center">${escHtml(
282
+ msg,
283
+ )}</p>`;
284
+ }
285
+
286
+ /* ── Syntax highlight ─────────────────────────────────────── */
287
+ function syntaxHighlight(obj) {
288
+ const json = JSON.stringify(obj, null, 2);
289
+ return json.replace(
290
+ /("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(?:\s*:)?|\b(true|false|null)\b|-?\d+\.?\d*(?:[eE][+-]?\d+)?)/g,
291
+ (m) => {
292
+ if (/^"/.test(m))
293
+ return /:$/.test(m)
294
+ ? `<span class="jk">${m}</span>`
295
+ : `<span class="js">${m}</span>`;
296
+ if (/true|false/.test(m)) return `<span class="jb">${m}</span>`;
297
+ if (/null/.test(m)) return `<span class="jz">${m}</span>`;
298
+ return `<span class="jn">${m}</span>`;
299
+ },
300
+ );
301
+ }
302
+
303
+ function escHtml(s) {
304
+ return String(s)
305
+ .replace(/&/g, '&amp;')
306
+ .replace(/</g, '&lt;')
307
+ .replace(/>/g, '&gt;');
308
+ }
309
+
310
+ /* ── Event log ────────────────────────────────────────────── */
311
+ function appendEvent(label, data, cssClass = 'log-event') {
312
+ const wasAtBottom =
313
+ $eventsLog.scrollHeight - $eventsLog.scrollTop <=
314
+ $eventsLog.clientHeight + 4;
315
+ const ts = new Date().toLocaleTimeString('en-US', { hour12: false });
316
+
317
+ const placeholder = document.getElementById('eventsPlaceholder');
318
+ if (placeholder) placeholder.remove();
319
+
320
+ const row = document.createElement('div');
321
+ row.className = `${cssClass} mb-2 pl-3 py-1 rounded-sm`;
322
+ row.innerHTML = `
323
+ <div class="flex items-center gap-2 mb-0.5">
324
+ <span class="text-gray-500 text-xs tabular-nums">${ts}</span>
325
+ <span class="text-gray-300 text-xs font-medium">${escHtml(
326
+ label,
327
+ )}</span>
328
+ </div>
329
+ ${
330
+ data !== undefined
331
+ ? `<pre class="text-gray-300">${
332
+ typeof data === 'object'
333
+ ? syntaxHighlight(data)
334
+ : `<span class="js">${escHtml(String(data))}</span>`
335
+ }</pre>`
336
+ : ''
337
+ }`;
338
+
339
+ $eventsLog.appendChild(row);
340
+ if (wasAtBottom) $eventsLog.scrollTop = $eventsLog.scrollHeight;
341
+ }
342
+
343
+ $clearBtn.addEventListener('click', () => {
344
+ $eventsLog.innerHTML =
345
+ '<p class="text-gray-600 italic text-xs text-center py-4">Cleared.</p>';
346
+ });
347
+
348
+ /* ── Active subscriptions panel ───────────────────────────── */
349
+ function renderSubs() {
350
+ if (activeSubs.size === 0) {
351
+ $subsSection.classList.add('hidden');
352
+ return;
353
+ }
354
+ $subsSection.classList.remove('hidden');
355
+ $subsList.innerHTML = '';
356
+
357
+ activeSubs.forEach((opts, subId) => {
358
+ const row = document.createElement('div');
359
+ row.className =
360
+ 'flex items-center justify-between bg-gray-50 rounded px-2 py-1 border border-gray-100 gap-2';
361
+ row.innerHTML = `
362
+ <div class="min-w-0 flex-1">
363
+ <span class="text-xs font-medium text-indigo-600">${escHtml(
364
+ opts.resource,
365
+ )}</span>
366
+ ${
367
+ opts.resourceId
368
+ ? `<span class="ml-1 text-xs text-gray-400">${escHtml(
369
+ opts.resourceId.slice(0, 8),
370
+ )}…</span>`
371
+ : ''
372
+ }
373
+ ${
374
+ opts.filter
375
+ ? `<span class="ml-1 text-xs text-gray-400">filter</span>`
376
+ : ''
377
+ }
378
+ <br/>
379
+ <span class="text-xs text-gray-400 font-mono">${escHtml(
380
+ subId,
381
+ )}</span>
382
+ </div>
383
+ <button data-subid="${escHtml(subId)}"
384
+ class="unsub-btn text-xs px-2 py-0.5 rounded bg-red-50 text-red-600 hover:bg-red-100 border border-red-200 transition shrink-0">
385
+ Unsub
386
+ </button>`;
387
+ $subsList.appendChild(row);
388
+ });
389
+
390
+ // Wire up Unsub buttons
391
+ $subsList.querySelectorAll('.unsub-btn').forEach((btn) => {
392
+ btn.addEventListener('click', () => doUnsubscribe(btn.dataset.subid));
393
+ });
394
+ }
395
+
396
+ /* ── Template buttons ─────────────────────────────────────── */
397
+ document.querySelectorAll('.tpl-btn').forEach((btn) => {
398
+ btn.addEventListener('click', () => {
399
+ const key = btn.dataset.tpl;
400
+ const tpl = TEMPLATES[key];
401
+ if (tpl) $subInput.value = JSON.stringify(tpl(), null, 2);
402
+ $subError.classList.add('hidden');
403
+ });
404
+ });
405
+
406
+ /* ── Subscribe ────────────────────────────────────────────── */
407
+ $btnSubscribe.addEventListener('click', () => doSubscribe());
408
+ $subInput.addEventListener('keydown', (e) => {
409
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') doSubscribe();
410
+ });
411
+
412
+ async function doSubscribe() {
413
+ $subError.classList.add('hidden');
414
+ if (!websocketProxy) {
415
+ showSubError('SO proxy not available. Is the host connected?');
416
+ return;
417
+ }
418
+
419
+ let opts;
420
+ try {
421
+ opts = JSON.parse($subInput.value || '{}');
422
+ } catch {
423
+ showSubError('Invalid JSON');
424
+ return;
425
+ }
426
+
427
+ if (!opts.resource) {
428
+ showSubError('"resource" is required');
429
+ return;
430
+ }
431
+
432
+ $btnSubscribe.disabled = true;
433
+ $btnSubscribe.textContent = 'Subscribing…';
434
+
435
+ try {
436
+ const result = await websocketProxy.subscribe(opts);
437
+ const { subscriptionId } = result;
438
+
439
+ activeSubs.set(subscriptionId, opts);
440
+ renderSubs();
441
+
442
+ appendEvent(
443
+ '✅ subscribe_ack',
444
+ { subscriptionId, ...opts },
445
+ 'log-sub',
446
+ );
447
+ notifyHost('subscribe-ack', { subscriptionId, ...opts });
448
+ } catch (err) {
449
+ const msg = err?.message ?? String(err);
450
+ showSubError(msg);
451
+ appendEvent('❌ subscribe error', { message: msg }, 'log-error');
452
+ notifyHost('guest-error', { message: msg, context: 'subscribe' });
453
+ } finally {
454
+ $btnSubscribe.disabled = false;
455
+ $btnSubscribe.textContent = 'Subscribe';
456
+ }
457
+ }
458
+
459
+ /* ── Unsubscribe ──────────────────────────────────────────── */
460
+ async function doUnsubscribe(subscriptionId) {
461
+ if (!websocketProxy) return;
462
+ try {
463
+ await websocketProxy.unsubscribe(subscriptionId);
464
+ activeSubs.delete(subscriptionId);
465
+ renderSubs();
466
+ appendEvent('🗑️ unsubscribe_ack', { subscriptionId }, 'log-unsub');
467
+ notifyHost('unsubscribe-ack', { subscriptionId });
468
+ } catch (err) {
469
+ const msg = err?.message ?? String(err);
470
+ appendEvent('❌ unsubscribe error', { message: msg }, 'log-error');
471
+ notifyHost('guest-error', {
472
+ message: msg,
473
+ context: 'unsubscribe',
474
+ subscriptionId,
475
+ });
476
+ }
477
+ }
478
+
479
+ function showSubError(msg) {
480
+ $subError.textContent = msg;
481
+ $subError.classList.remove('hidden');
482
+ }
483
+
484
+ /* ── SSF Guest connect + get proxy ────────────────────────── */
485
+ async function initGuest() {
486
+ const logger = buildConsoleLogger('SSFGuest');
487
+
488
+ // SSFGuest can use console: true so it doesn't hit a remote logging endpoint
489
+ const guest = new ice.guest.SSFGuest({ logger: { console: true } });
490
+
491
+ logger.debug('Connecting to SSF Host…');
492
+ await guest.connect();
493
+ logger.debug('Connected. Getting websocket object…');
494
+
495
+ websocketProxy = await guest.getObject('websocket');
496
+ logger.debug('Got websocket proxy');
497
+
498
+ // Listen for events from the SO
499
+ websocketProxy.Message.subscribe((eventData) => {
500
+ const { eventParams } = eventData;
501
+ appendEvent('⚡ message event', eventParams, 'log-event');
502
+ });
503
+
504
+ setConnected();
505
+ notifyHost('guest-ready', {});
506
+ }
507
+
508
+ /* ── Boot ─────────────────────────────────────────────────── */
509
+ window.addEventListener('load', () => {
510
+ if (!window.ice?.guest?.SSFGuest) {
511
+ setError('SSF Guest CDN failed to load. Check network and reload.');
512
+ return;
513
+ }
514
+ initGuest().catch((err) => {
515
+ const msg = err?.message ?? String(err);
516
+ setError(msg);
517
+ notifyHost('guest-error', { message: msg, context: 'init' });
518
+ console.error('[Guest] Init failed:', err);
519
+ });
520
+ });
521
+ </script>
522
+ </body>
523
+ </html>
@@ -0,0 +1 @@
1
+ <!doctype html><html lang="en" class="h-full"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>WebSocket SO Playground</title><script src="https://cdn.tailwindcss.com?plugins=forms"></script><style>.jk{color:#c084fc}.js{color:#4ade80}.jn{color:#fb923c}.jb{color:#60a5fa}.jz{color:#6b7280}.log-info{border-left:3px solid #6366f1}.log-open{border-left:3px solid #22c55e}.log-close{border-left:3px solid #f59e0b}.log-event{border-left:3px solid #818cf8}.log-error{border-left:3px solid #f87171}.log-sub{border-left:3px solid #34d399}.log-unsub{border-left:3px solid #94a3b8}.scroll-panel{overflow-y:auto}pre{white-space:pre-wrap;word-break:break-all;font-size:.75rem}#guest-area{height:560px}#guest-area iframe{border:0;width:100%;height:100%}</style><script defer="defer" src="index.js"></script></head><body class="h-full flex flex-col bg-gray-100 text-sm text-gray-800 font-sans"><header class="flex items-center justify-between px-4 py-2 bg-indigo-700 text-white shrink-0 shadow"><div class="flex items-center gap-3"><span class="font-semibold tracking-wide">WebSocket SO Playground</span> <span class="text-indigo-300 text-xs hidden sm:inline">@elliemae/pui-websocket-so · Phase II Service Orders</span></div><div class="flex items-center gap-4"><span id="timer" class="text-xs text-indigo-200 tabular-nums hidden">00:00:00</span> <span id="stats" class="text-xs text-indigo-200 hidden"><span title="Events dispatched">⚡ <span id="statEvents">0</span></span> <span class="ml-2" title="Active subscriptions across all guests">⬡ <span id="statSubs">0</span></span> </span><span id="statusPill" class="bg-gray-100 text-gray-600 inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium">● Disconnected</span></div></header><main class="flex flex-1 overflow-hidden"><aside class="w-72 shrink-0 bg-white border-r border-gray-200 flex flex-col overflow-hidden"><div class="scroll-panel flex-1 p-4 space-y-5"><section><h2 class="text-xs font-semibold uppercase tracking-widest text-gray-400 mb-3">Server Details</h2><div class="mb-3"><label class="block text-xs font-medium text-gray-700 mb-1" for="wsEnvironments">Environment</label> <select id="wsEnvironments" class="block w-full rounded border-gray-300 text-xs py-1.5 focus:ring-indigo-500 focus:border-indigo-500"><option value="other" selected="selected">Local (other)</option><option value="wss://dev.api.ellielabs.com">PSS-Dev</option><option value="wss://qa.api.ellielabs.com">PSS-Qa</option><option value="wss://int.api.ellielabs.com">PSS-Int</option><option value="wss://peg.api.ellielabs.com">PSS-Peg</option><option value="wss://stg.api.elliemae.com">PSS-Stg</option><option value="wss://api.elliemae.com">PSS-Prod</option></select></div><div id="wsCustomDomainField" class="mb-3"><label class="block text-xs font-medium text-gray-700 mb-1" for="wsCustomDomain">Server Domain</label> <input id="wsCustomDomain" autocomplete="off" value="ws://localhost:5000" placeholder="ws://localhost:5000" class="block w-full rounded border-gray-300 text-xs py-1.5 focus:ring-indigo-500 focus:border-indigo-500"/></div><div class="mb-3"><label class="block text-xs font-medium text-gray-700 mb-1" for="wsPath">WS Path</label> <input id="wsPath" autocomplete="off" value="encompass/v1/stream" placeholder="encompass/v1/stream" class="block w-full rounded border-gray-300 text-xs py-1.5 focus:ring-indigo-500 focus:border-indigo-500"/></div><div class="mb-4"><label class="block text-xs font-medium text-gray-700 mb-1" for="wsToken">Auth Token</label> <input id="wsToken" autocomplete="off" value="12345" placeholder="Bearer token..." class="block w-full rounded border-gray-300 text-xs py-1.5 font-mono focus:ring-indigo-500 focus:border-indigo-500"/></div><div class="flex gap-2"><button id="btnConnect" class="flex-1 rounded bg-indigo-600 text-white text-xs font-medium py-1.5 hover:bg-indigo-700 active:bg-indigo-800 transition">Connect</button> <button id="btnDisconnect" disabled="disabled" class="flex-1 rounded bg-gray-200 text-gray-400 text-xs font-medium py-1.5 cursor-not-allowed transition">Disconnect</button></div><div id="connectError" class="hidden mt-2 text-xs text-red-600 bg-red-50 border border-red-200 rounded p-2"></div></section><section id="guestPanel" class="hidden"><h2 class="text-xs font-semibold uppercase tracking-widest text-gray-400 mb-3">Guest Simulator</h2><div id="guestStatus" class="text-xs text-gray-500 bg-gray-50 rounded border border-gray-200 p-2">Loading guest…</div></section><section class="text-xs text-gray-400 space-y-1 pt-2"><p class="font-medium text-gray-500">How this works</p><p>1. Click <strong>Connect</strong> to open the WebSocket and register the SO with SSF Host.</p><p>2. The guest iframe loads automatically and connects to the host via SSF Guest.</p><p>3. Use the guest panel to subscribe and receive live events.</p></section></div></aside><div class="flex-1 flex flex-col overflow-hidden"><div class="flex flex-col border-b border-gray-200" style="height:580px"><div class="flex items-center justify-between px-4 py-2 bg-white border-b border-gray-200 shrink-0"><h2 class="text-xs font-semibold uppercase tracking-widest text-gray-400">Guest Simulator</h2><span id="guestFrameLabel" class="text-xs text-gray-400 italic">Connect to load guest</span></div><div id="guest-placeholder" class="flex flex-col items-center justify-center flex-1 bg-gray-50 text-gray-400 gap-2"><svg class="w-10 h-10 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 17v-2a4 4 0 014-4h4m0 0l-3-3m3 3l-3 3M9 7H5a2 2 0 00-2 2v8a2 2 0 002 2h4"/></svg><p class="text-sm">Connect to load the guest simulator</p></div><div id="guest-area" class="hidden flex-1 overflow-hidden bg-white"></div></div><div class="flex flex-col flex-1 overflow-hidden"><div class="flex items-center justify-between px-4 py-2 bg-white border-b border-gray-200 shrink-0"><h2 class="text-xs font-semibold uppercase tracking-widest text-gray-400">Host Event Log</h2><button id="btnClearLog" class="text-xs text-gray-500 hover:text-red-500 transition">Clear</button></div><div id="hostLog" class="scroll-panel flex-1 p-3 bg-gray-950 font-mono"><p class="text-gray-600 italic text-xs text-center py-4">Events dispatched through the SO will appear here.</p></div></div></div></main><script src="https://assets.ellieservices.com/pui-diagnostics@3.2.0/umd/index.js" async></script><script src="https://cdn.mortgagetech.ice.com/ssf-host" async></script><script type="module">const GUEST_ID="wsso-guest-1",GUEST_URL="/guest.html",ANALYTICS_MOCK={sendBAEvent:async()=>{},startTiming:async()=>{},endTiming:async()=>{}};let hostInstance=null,soInstance=null,connected=!1,timerInterval=null,connectedAt=null,eventsDispatched=0,activeSubs=0;const $pill=document.getElementById("statusPill"),$timer=document.getElementById("timer"),$stats=document.getElementById("stats"),$statEvents=document.getElementById("statEvents"),$statSubs=document.getElementById("statSubs"),$btnConnect=document.getElementById("btnConnect"),$btnDisconn=document.getElementById("btnDisconnect"),$connectErr=document.getElementById("connectError"),$guestPanel=document.getElementById("guestPanel"),$guestStatus=document.getElementById("guestStatus"),$guestLabel=document.getElementById("guestFrameLabel"),$placeholder=document.getElementById("guest-placeholder"),$guestArea=document.getElementById("guest-area"),$hostLog=document.getElementById("hostLog"),$clearBtn=document.getElementById("btnClearLog"),$env=document.getElementById("wsEnvironments"),$domain=document.getElementById("wsCustomDomain"),$path=document.getElementById("wsPath");function getWsUrl(){return`${"other"===$env.value?$domain.value.trim().replace(/\/$/,""):$env.value}/${$path.value.trim().replace(/^\//,"")}`}function setStatus(e,t){const n={gray:"bg-gray-100 text-gray-600",yellow:"bg-yellow-100 text-yellow-700",green:"bg-green-100 text-green-700",red:"bg-red-100 text-red-700"};$pill.className=`${n[t]??n.gray} inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium`,$pill.textContent=`● ${e}`}function startTimer(){connectedAt=Date.now(),$timer.classList.remove("hidden"),$stats.classList.remove("hidden"),timerInterval=setInterval(()=>{const e=Math.floor((Date.now()-connectedAt)/1e3),t=String(Math.floor(e/3600)).padStart(2,"0"),n=String(Math.floor(e%3600/60)).padStart(2,"0"),s=String(e%60).padStart(2,"0");$timer.textContent=`${t}:${n}:${s}`},1e3)}function stopTimer(){clearInterval(timerInterval),$timer.classList.add("hidden"),$stats.classList.add("hidden")}function waitForGlobals(e,t=15e3){return new Promise((n,s)=>{const o=Date.now()+t,a=setInterval(()=>{if(e.every(e=>void 0!==e.split(".").reduce((e,t)=>e?.[t],window)))return clearInterval(a),void n();Date.now()>o&&(clearInterval(a),s(new Error(`Timed out waiting for: ${e.join(", ")}`)))},50)})}function buildLogger(e="WebSocketSO"){const t=`[${e}]`;if(window.emuiDiagnostics){const{logger:t,http:n}=window.emuiDiagnostics;try{return t({transport:n("https://int.api.ellielabs.com/diagnostics/v2/logging"),index:"wsso-playground",team:"ui platform",appName:e})}catch{}}return{debug:(e,...n)=>console.debug(t,e,...n),info:(e,...n)=>console.info(t,e,...n),warn:(e,...n)=>console.warn(t,e,...n),error:(e,...n)=>console.error(t,e,...n),audit:(e,...n)=>console.info(t,"[AUDIT]",e,...n),setLogLevel:()=>{},getLogLevel:()=>"debug"}}function syntaxHighlight(e){return JSON.stringify(e,null,2).replace(/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(?:\s*:)?|\b(true|false|null)\b|-?\d+\.?\d*(?:[eE][+-]?\d+)?)/g,e=>/^"/.test(e)?/:$/.test(e)?`<span class="jk">${e}</span>`:`<span class="js">${e}</span>`:/true|false/.test(e)?`<span class="jb">${e}</span>`:/null/.test(e)?`<span class="jz">${e}</span>`:`<span class="jn">${e}</span>`)}function appendLog(e,t,n,s="log-info"){const o=$hostLog.scrollHeight-$hostLog.scrollTop<=$hostLog.clientHeight+4,a=(new Date).toLocaleTimeString("en-US",{hour12:!1}),r=document.createElement("div");r.className=`${s} mb-2 pl-3 py-1 rounded-sm`;const c=document.createElement("div");if(c.className="flex items-center gap-2 mb-0.5",c.innerHTML=`\n <span class="text-gray-500 text-xs tabular-nums">${a}</span>\n <span class="text-gray-300 text-xs font-medium">${t}</span>`,r.appendChild(c),void 0!==n){const e=document.createElement("pre");e.className="text-gray-300",e.innerHTML="object"==typeof n?syntaxHighlight(n):`<span class="js">${String(n)}</span>`,r.appendChild(e)}const d=$hostLog.querySelector("p.italic");d&&d.remove(),$hostLog.appendChild(r),o&&($hostLog.scrollTop=$hostLog.scrollHeight)}function showGuestFrame(){$placeholder.classList.add("hidden"),$guestArea.classList.remove("hidden"),$guestLabel.textContent=`Guest ID: ${GUEST_ID}`,$guestPanel.classList.remove("hidden"),$guestStatus.textContent="Loading…"}function showGuestPlaceholder(){$guestArea.classList.add("hidden"),$guestArea.innerHTML="",$placeholder.classList.remove("hidden"),$guestLabel.textContent="Connect to load guest",$guestPanel.classList.add("hidden")}function patchHostDispatch(e){const t=e.dispatchEvent.bind(e);e.dispatchEvent=async e=>{const{event:n,eventParams:s,eventOptions:o}=e;return eventsDispatched++,$statEvents.textContent=String(eventsDispatched),appendLog("event",`⚡ dispatch → guest "${o?.guestId??"*"}"`,{event:n,eventParams:s,eventOptions:o},"log-event"),t(e)}}function showError(e){$connectErr.textContent=e,$connectErr.classList.remove("hidden")}$env.addEventListener("change",()=>{const e="other"===$env.value;document.getElementById("wsCustomDomainField").style.display=e?"":"none"}),$clearBtn.addEventListener("click",()=>{$hostLog.innerHTML='<p class="text-gray-600 italic text-xs text-center py-4">Cleared.</p>'}),$btnConnect.addEventListener("click",async()=>{$connectErr.classList.add("hidden"),$btnConnect.disabled=!0,setStatus("Connecting…","yellow");try{await waitForGlobals(["emuiWebsocketSo.WebSocketSO","ice.host.SSFHost"])}catch(e){return showError(`CDN not loaded: ${e.message}`),$btnConnect.disabled=!1,void setStatus("Disconnected","gray")}const e=getWsUrl(),t=document.getElementById("wsToken").value.trim(),n=buildLogger("WebSocketSO");try{hostInstance||(hostInstance=new ice.host.SSFHost("wsso-playground",{logger:n,analyticsObj:ANALYTICS_MOCK}),patchHostDispatch(hostInstance),appendLog("info","🏠 SSFHost created",{hostId:"wsso-playground"}));try{hostInstance.removeScriptingObject("websocket")}catch{}soInstance=new emuiWebsocketSo.WebSocketSO({logger:n,host:hostInstance,url:e,token:t,onError:e=>{appendLog("error","🔴 SO error",{message:e.message},"log-error"),setStatus("Error","red")}}),appendLog("info","⚙️ WebSocketSO created",{url:e,token:"***"}),hostInstance.addScriptingObject(soInstance),appendLog("info",'📋 SO registered with SSFHost as "websocket"'),await soInstance.open(),appendLog("open","🟢 WebSocket opened",{url:e},"log-open"),connected=!0,setStatus("Connected","green"),startTimer(),eventsDispatched=0,$statEvents.textContent="0",$statSubs.textContent="0",$btnConnect.disabled=!1,$btnConnect.classList.add("opacity-50","cursor-not-allowed"),$btnConnect.disabled=!0,$btnDisconn.disabled=!1,$btnDisconn.classList.remove("bg-gray-200","text-gray-400","cursor-not-allowed"),$btnDisconn.classList.add("bg-red-600","text-white","hover:bg-red-700"),showGuestFrame(),hostInstance.loadGuest({id:GUEST_ID,url:GUEST_URL,title:"Guest Simulator",targetElement:$guestArea}),appendLog("info",`👤 loadGuest("${GUEST_ID}")`,{url:GUEST_URL}),$guestStatus.textContent="Guest loaded. Connecting via SSF…"}catch(e){showError(e.message??String(e)),setStatus("Error","red"),$btnConnect.disabled=!1}}),$btnDisconn.addEventListener("click",async()=>{$btnDisconn.disabled=!0;try{hostInstance.unloadGuest(GUEST_ID)}catch{}if(showGuestPlaceholder(),soInstance){try{soInstance.close(),appendLog("close","🔴 WebSocket closed",void 0,"log-close")}catch{}try{hostInstance.removeScriptingObject("websocket")}catch{}soInstance=null}connected=!1,setStatus("Disconnected","gray"),stopTimer(),$btnConnect.disabled=!1,$btnConnect.classList.remove("opacity-50","cursor-not-allowed"),$btnDisconn.disabled=!0,$btnDisconn.classList.add("bg-gray-200","text-gray-400","cursor-not-allowed"),$btnDisconn.classList.remove("bg-red-600","text-white","hover:bg-red-700")}),window.addEventListener("message",e=>{if(!0!==e.data?.__soPlayground)return;const{type:t,payload:n}=e.data;"guest-ready"===t&&($guestStatus.innerHTML='<span class="text-green-600 font-medium">✓ Guest connected and SO proxy retrieved</span>',appendLog("info","👤 Guest ready",{guestId:GUEST_ID},"log-sub")),"subscribe-ack"===t&&(activeSubs++,$statSubs.textContent=String(activeSubs),appendLog("sub","✅ Guest subscribed",n,"log-sub")),"unsubscribe-ack"===t&&(activeSubs=Math.max(0,activeSubs-1),$statSubs.textContent=String(activeSubs),appendLog("unsub","🗑️ Guest unsubscribed",n,"log-unsub")),"guest-error"===t&&appendLog("error","❌ Guest error",n,"log-error")})</script></body></html>
@@ -0,0 +1,3 @@
1
+ (function(g,l){typeof exports=="object"&&typeof module=="object"?module.exports=l():typeof define=="function"&&define.amd?define([],l):typeof exports=="object"?exports.emuiWebsocketSo=l():g.emuiWebsocketSo=l()})(globalThis,()=>(()=>{"use strict";var h={};h.d=(i,e)=>{for(var t in e)h.o(e,t)&&!h.o(i,t)&&Object.defineProperty(i,t,{enumerable:!0,get:e[t]})},h.o=(i,e)=>Object.prototype.hasOwnProperty.call(i,e),h.r=i=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(i,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(i,"__esModule",{value:!0})};var g={};h.r(g),h.d(g,{WebSocketSO:()=>N});class l{name;objectId;id;constructor(e){const{name:t,objectId:s}=e;if(!t)throw new Error("Event name is required");if(!s)throw new Error("Scripting object id is required");this.objectId=s,this.name=t,this.id=`${this.objectId}.${this.name}`.toLowerCase()}}class q{static[Symbol.hasInstance](e){return typeof e=="object"&&e!==null&&"getType"in e&&typeof e.getType=="function"&&e.getType()==="ProxyEvent"}#e;objectId;name;id;getType(){return"ProxyEvent"}constructor(e){const{name:t,objectId:s,eventSrc:r}=e;if(!t)throw new Error("Event name is required");if(!s)throw new Error("Scripting object id is required");if(!r)throw new Error("Event source is required");this.objectId=s,this.name=t,this.#e=r,this.id=`${this.objectId}.${this.name}`.toLowerCase()}subscribe=e=>this.#e.subscribe({eventId:this.id,callback:e});unsubscribe=e=>{this.#e.unsubscribe({eventId:this.id,token:e})}}const v=i=>i instanceof l,R=(i,e)=>`${i.toLowerCase()}.${e.toLowerCase()}`,$="function",S=(i,e)=>typeof i===$&&!!e&&!e.startsWith("_");class y{#e;#t="Object";constructor(e,t){this.#e=e,this.#t=t||this.#t}get id(){return this.#e}get objectType(){return this.#t}_toJSON=()=>{const e=[],t=[];return Object.keys(this).forEach(s=>{const r=this[s];v(r)?t.push(s):S(r,s)&&e.push(s)}),{objectId:this.#e,objectType:this.#t,functions:e,events:t}};_dispose=()=>{};dispose=()=>{}}var b=(i=>(i.JSON="application/json",i.BINARY="application/octet-stream",i.RAW="",i))(b||{}),T=(i=>(i[i.UNINSTANTIATED=-1]="UNINSTANTIATED",i[i.CONNECTING=0]="CONNECTING",i[i.OPEN=1]="OPEN",i[i.CLOSING=2]="CLOSING",i[i.CLOSED=3]="CLOSED",i))(T||{});const E=1*60*60,j=5;class O{#e=null;#t;#r=b.JSON;#s=E;#n=j;#i;#o;#h;#u;#c;constructor({url:e,token:t,logger:s,contentType:r=b.JSON,onMessage:n,onError:o}){if(!e)throw new Error("url is required");if(!t)throw new Error("token is required");if(!s)throw new Error("logger is required");this.#t=s,this.#r=r,this.#h=n,this.#i=e,this.#o=t,this.#u=o}#d=()=>{this.#t.debug(`Connected to ${this.#i}`),setTimeout(()=>{this.#c?.()},0)};#a=(e,t)=>{const{code:s,reason:r}=t;if(`${s}`.startsWith("4")&&s!==4408){this.#t.error(`Connection to ${this.#i} closed with code ${s} and reason ${r}`),setTimeout(()=>{this.#u?.(new Error(r))},0),e(new Error(r));return}if(this.#n<=0){this.#t.error(`Unable to connect to ${this.#i}. Reason: ${r}. Max retries reached`),setTimeout(()=>{this.#u?.(new Error(`Unable to connect to ${this.#i}. Reason: ${r}`))},0);return}setTimeout(()=>{this.open().catch(()=>{}),this.#n-=1,this.#s+=this.#s},this.#s)};#l=e=>{let t;switch(this.#r){case b.JSON:try{t=JSON.parse(e.data)}catch(s){this.#t.error(`Error parsing message: ${s.message}. Received message is not a valid JSON`)}break;default:t=e.data;break}setTimeout(()=>{try{this.#h(t)}catch(s){this.#t.debug(`User message handler throws error: ${s.message}`)}},0)};get readyState(){return this.#e?.readyState}set onOpen(e){this.#c=e}open=()=>new Promise((e,t)=>{const s=`auth--${this.#o}`;this.#e=new WebSocket(this.#i,[s]),this.#e.onopen=()=>{this.#d(),e()},this.#e.onerror=()=>{this.#t.error(`Error connecting to websocket server at ${this.#i}`)},this.#e.onclose=this.#a.bind(this,t),this.#e.onmessage=this.#l});close=()=>{this.#s=E,this.#e&&(this.#e.onclose=null,this.#e.onmessage=null,this.#e.onerror=null,this.#e.onopen=null,this.#e.close(),this.#e=null)};send=e=>new Promise((t,s)=>{if(!this.#e){s(new Error("Connection is not open"));return}switch(this.#r){case b.JSON:try{const r=JSON.stringify(e);this.#e.send(r),t()}catch(r){const n=`Error sending message: ${r.message}. Message is not serializable as JSON`;this.#t.error(n),s(new Error(n))}break;default:this.#e.send(e),t();break}})}const k=3e4;class d{#e=new Map;#t=new Map;#r=new Map;#s;constructor(e=k){this.#s=e}addPendingRequest(e,t,s){const r=this.#e.get(e);r&&(clearTimeout(r.timeoutId),r.reject(new Error(`Pending request replaced (correlationId: ${e})`)));const n=setTimeout(()=>{this.#e.delete(e),s(new Error(`Request timed out after ${this.#s}ms (correlationId: ${e})`))},this.#s);this.#e.set(e,{resolve:t,reject:s,timeoutId:n})}resolvePendingRequest(e,t){const s=this.#e.get(e);return s?(clearTimeout(s.timeoutId),this.#e.delete(e),s.resolve(t),!0):!1}rejectPendingRequest(e,t){const s=this.#e.get(e);return s?(clearTimeout(s.timeoutId),this.#e.delete(e),s.reject(t),!0):!1}findExistingSubscription(e){const t=d.#n(e),s=this.#r.get(t);if(s)return this.#t.get(s)}addSubscription(e){const t={...e,filter:e.filter?JSON.parse(JSON.stringify(e.filter)):void 0,refCount:1};this.#t.set(e.subscriptionId,t),this.#r.set(d.#n(t),e.subscriptionId)}incrementRefCount(e){const t=this.#t.get(e);return t?(t.refCount+=1,t.refCount):-1}decrementRefCount(e){const t=this.#t.get(e);return t?(t.refCount-=1,t.refCount<=0?(this.#r.delete(d.#n(t)),this.#t.delete(e),0):t.refCount):-1}removeSubscription(e){const t=this.#t.get(e);return t&&this.#r.delete(d.#n(t)),this.#t.delete(e)}getSubscription(e){return this.#t.get(e)}hasSubscription(e){return this.#t.has(e)}getAllSubscriptions(){return Array.from(this.#t.values())}dispose(){this.#e.forEach(e=>{clearTimeout(e.timeoutId),e.reject(new Error("WebSocket scripting object disposed"))}),this.#e.clear(),this.#t.clear(),this.#r.clear()}static buildLookupKey(e){return d.#n(e)}static#n(e){const{guestId:t,resource:s,resourceId:r,filter:n}=e;if(r!=null)return`${t}\0${s}\0rid:${r}`;if(n!=null){const c=Object.keys(n).sort().map(u=>`${u}=${n[u].join(",")}`).join("&");return`${t}\0${s}\0f:${c}`}return`${t}\0${s}`}}class C{#e;#t;#r;constructor(e,t,s){this.#e=e,this.#t=t,this.#r=s}handleMessage=e=>{switch(e.type){case"subscribe_ack":this.#s(e);break;case"unsubscribe_ack":this.#n(e);break;case"error":this.#i(e);break;case"event":this.#o(e);break;default:this.#r.debug(`Unknown message type received: ${e.type}`)}};#s(e){const{correlationId:t,subscriptionId:s}=e;this.#e.resolvePendingRequest(t,{subscriptionId:s})||this.#r.debug(`Received subscribe_ack for unknown correlationId: ${t}`)}#n(e){const{correlationId:t}=e;this.#e.resolvePendingRequest(t,void 0)||this.#r.debug(`Received unsubscribe_ack for unknown correlationId: ${t}`)}#i(e){const{correlationId:t,errors:s}=e,r=s.map(o=>`${o.code}: ${o.message}`).join("; ");this.#e.rejectPendingRequest(t,new Error(r))||this.#r.debug(`Received error for unknown correlationId: ${t}. Errors: ${r}`)}#o(e){const{subscriptionId:t}=e,s=this.#e.getSubscription(t);if(!s){this.#r.debug(`Received event for unknown subscriptionId: ${t}`);return}this.#t(e,s)}}const w="websocket",I="message",P=`${w}.${I}`;class N extends y{#e;#t;#r;#s;#n;#i=!1;#o=new Map;Message=new l({name:I,objectId:w});constructor({logger:e,host:t,url:s,token:r,onError:n}){if(super(w),!e)throw new Error("logger is required");if(!t)throw new Error("host is required");if(!s)throw new Error("url is required");if(!r)throw new Error("token is required");this.#t=e,this.#r=t,this.#s=new d,this.#n=new C(this.#s,this.#a,this.#t),this.#e=new O({url:s,token:r,logger:e,contentType:b.JSON,onMessage:o=>{this.#n.handleMessage(o)},onError:n}),this.#e.onOpen=this.#d,Object.defineProperty(this,"open",{enumerable:!1}),Object.defineProperty(this,"close",{enumerable:!1})}open=async()=>{this.#i||(await this.#e.open(),this.#i=!0)};close=()=>{this.#i&&(this.#i=!1,this.#o.clear(),this.#e.close(),this.#s.dispose())};#h=e=>{const t=this.subscribe.callContext?.guest?.id;if(!t)throw new Error("unable to identify calling guest from callContext");if(!this.#i)throw new Error("WebSocket connection is not open. Call open() first.");const{resource:s,resourceId:r,filter:n}=e;if(!s)throw new Error("resource is required");if(!r&&!n)throw new Error("either resourceId or filter must be provided");return{guestId:t,resource:s,resourceId:r,filter:n}};subscribe=e=>{let t;try{t=this.#h(e)}catch(f){return Promise.reject(f)}const{guestId:s,resource:r,resourceId:n,filter:o}=t,c={resource:r,guestId:s,resourceId:n,filter:o},u=this.#s.findExistingSubscription(c);if(u)return this.#s.incrementRefCount(u.subscriptionId),Promise.resolve({subscriptionId:u.subscriptionId});const a=d.buildLookupKey(c),m=this.#o.get(a);return m?m.then(f=>(this.#s.incrementRefCount(f.subscriptionId),f)):this.#u(c,a)};#u=(e,t)=>{const{guestId:s,resource:r,resourceId:n,filter:o}=e,c=crypto.randomUUID(),u={type:"subscribe",correlationId:c,resource:r,...n!=null&&{resourceId:n},...o!=null&&{filter:o}},a=new Promise((m,f)=>{this.#s.addPendingRequest(c,p=>{this.#s.addSubscription({subscriptionId:p.subscriptionId,resource:r,guestId:s,resourceId:n,filter:o}),this.#t.debug(`Guest ${s} subscribed to ${r} with subscriptionId: ${p.subscriptionId}`),m(p)},f),this.#c(u).catch(p=>{this.#s.rejectPendingRequest(c,p)})});return this.#o.set(t,a),a.finally(()=>this.#o.delete(t)).catch(()=>{}),a};unsubscribe=e=>{if(!e)return Promise.reject(new Error("subscriptionId is required"));if(!this.#i)return Promise.reject(new Error("WebSocket connection is not open. Call open() first."));const t=this.unsubscribe.callContext?.guest?.id;if(!t)return Promise.reject(new Error("unable to identify calling guest from callContext"));const s=this.#s.getSubscription(e);if(!s||s.guestId!==t)return Promise.reject(new Error(`subscription ${e} is not available for this guest`));const r=this.#s.decrementRefCount(e);if(r>0)return this.#t.debug(`Decremented refCount for ${e} to ${r}`),Promise.resolve();const n=crypto.randomUUID(),o={type:"unsubscribe",correlationId:n,subscriptionId:e};return new Promise((c,u)=>{this.#s.addPendingRequest(n,()=>{this.#t.debug(`Unsubscribed from subscriptionId: ${e}`),c()},u),this.#c(o).catch(a=>{this.#s.rejectPendingRequest(n,a)})})};#c=e=>this.#e.send(e);#d=()=>{const e=this.#s.getAllSubscriptions();e.length!==0&&(this.#t.debug(`WebSocket reconnected \u2014 re-subscribing ${e.length} subscription(s)`),e.forEach(t=>{const s=crypto.randomUUID(),r={type:"subscribe",correlationId:s,resource:t.resource,...t.resourceId!=null&&{resourceId:t.resourceId},...t.filter!=null&&{filter:t.filter}};this.#s.addPendingRequest(s,n=>{this.#s.removeSubscription(t.subscriptionId),this.#s.addSubscription({...t,subscriptionId:n.subscriptionId}),this.#t.debug(`Re-subscribed guest ${t.guestId} to ${t.resource}: ${t.subscriptionId} \u2192 ${n.subscriptionId}`)},n=>{this.#s.removeSubscription(t.subscriptionId),this.#t.error(`Failed to re-subscribe guest ${t.guestId} to ${t.resource}: ${n.message}. Subscription removed.`)}),this.#c(r).catch(n=>{this.#s.rejectPendingRequest(s,n)})}))};#a=(e,t)=>{const s={subscriptionId:e.subscriptionId,resource:e.resource,payload:e.payload};this.#r.dispatchEvent({event:{id:P,name:I},eventParams:s,eventOptions:{guestId:t.guestId}}).catch(r=>{this.#t.error(`Failed to dispatch event to guest ${t.guestId}: ${r.message}`)})};_dispose=()=>{this.close()}}return g})());
2
+
3
+ //# sourceMappingURL=index.js.map
Binary file
Binary file