@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.
- package/dist/cjs/index.html +758 -0
- package/dist/cjs/index.js +24 -0
- package/dist/cjs/messageRouter.js +111 -0
- package/dist/cjs/package.json +7 -0
- package/dist/cjs/subscriptionManager.js +224 -0
- package/dist/cjs/types.js +16 -0
- package/dist/cjs/websocketSO.js +338 -0
- package/dist/esm/index.html +758 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/messageRouter.js +91 -0
- package/dist/esm/package.json +7 -0
- package/dist/esm/subscriptionManager.js +204 -0
- package/dist/esm/types.js +0 -0
- package/dist/esm/websocketSO.js +318 -0
- package/dist/public/guest.html +523 -0
- package/dist/public/index.html +1 -0
- package/dist/public/js/emuiWebsocketSo.cc1f5b5e1d095fc3a34e.js +3 -0
- package/dist/public/js/emuiWebsocketSo.cc1f5b5e1d095fc3a34e.js.br +0 -0
- package/dist/public/js/emuiWebsocketSo.cc1f5b5e1d095fc3a34e.js.gz +0 -0
- package/dist/public/js/emuiWebsocketSo.cc1f5b5e1d095fc3a34e.js.map +1 -0
- package/dist/types/lib/index.d.ts +2 -0
- package/dist/types/lib/messageRouter.d.ts +30 -0
- package/dist/types/lib/subscriptionManager.d.ts +101 -0
- package/dist/types/lib/tests/messageRouter.test.d.ts +1 -0
- package/dist/types/lib/tests/subscriptionManager.test.d.ts +1 -0
- package/dist/types/lib/tests/websocketSO.test.d.ts +1 -0
- package/dist/types/lib/types.d.ts +118 -0
- package/dist/types/lib/websocketSO.d.ts +56 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/dist/umd/guest.html +523 -0
- package/dist/umd/index.html +1 -0
- package/dist/umd/index.js +3 -0
- package/dist/umd/index.js.br +0 -0
- package/dist/umd/index.js.gz +0 -0
- package/dist/umd/index.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="h-full">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>WebSocket SO Playground</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
/* ── JSON syntax colours ───────────────────────────── */
|
|
11
|
+
.jk {
|
|
12
|
+
color: #c084fc;
|
|
13
|
+
} /* key – purple-400 */
|
|
14
|
+
.js {
|
|
15
|
+
color: #4ade80;
|
|
16
|
+
} /* string – green-400 */
|
|
17
|
+
.jn {
|
|
18
|
+
color: #fb923c;
|
|
19
|
+
} /* number – orange-400 */
|
|
20
|
+
.jb {
|
|
21
|
+
color: #60a5fa;
|
|
22
|
+
} /* bool – blue-400 */
|
|
23
|
+
.jz {
|
|
24
|
+
color: #6b7280;
|
|
25
|
+
} /* null – gray-500 */
|
|
26
|
+
|
|
27
|
+
/* ── Log entry left-border colours ─────────────────── */
|
|
28
|
+
.log-info {
|
|
29
|
+
border-left: 3px solid #6366f1;
|
|
30
|
+
}
|
|
31
|
+
.log-open {
|
|
32
|
+
border-left: 3px solid #22c55e;
|
|
33
|
+
}
|
|
34
|
+
.log-close {
|
|
35
|
+
border-left: 3px solid #f59e0b;
|
|
36
|
+
}
|
|
37
|
+
.log-event {
|
|
38
|
+
border-left: 3px solid #818cf8;
|
|
39
|
+
}
|
|
40
|
+
.log-error {
|
|
41
|
+
border-left: 3px solid #f87171;
|
|
42
|
+
}
|
|
43
|
+
.log-sub {
|
|
44
|
+
border-left: 3px solid #34d399;
|
|
45
|
+
}
|
|
46
|
+
.log-unsub {
|
|
47
|
+
border-left: 3px solid #94a3b8;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ── Scrollable panels ─────────────────────────────── */
|
|
51
|
+
.scroll-panel {
|
|
52
|
+
overflow-y: auto;
|
|
53
|
+
}
|
|
54
|
+
pre {
|
|
55
|
+
white-space: pre-wrap;
|
|
56
|
+
word-break: break-all;
|
|
57
|
+
font-size: 0.75rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ── Guest container size ──────────────────────────── */
|
|
61
|
+
#guest-area {
|
|
62
|
+
height: 560px;
|
|
63
|
+
}
|
|
64
|
+
#guest-area iframe {
|
|
65
|
+
border: 0;
|
|
66
|
+
width: 100%;
|
|
67
|
+
height: 100%;
|
|
68
|
+
}
|
|
69
|
+
</style>
|
|
70
|
+
</head>
|
|
71
|
+
|
|
72
|
+
<body
|
|
73
|
+
class="h-full flex flex-col bg-gray-100 text-sm text-gray-800 font-sans"
|
|
74
|
+
>
|
|
75
|
+
<!-- ═══════════════════════════ HEADER ════════════════════════════ -->
|
|
76
|
+
<header
|
|
77
|
+
class="flex items-center justify-between px-4 py-2 bg-indigo-700 text-white shrink-0 shadow"
|
|
78
|
+
>
|
|
79
|
+
<div class="flex items-center gap-3">
|
|
80
|
+
<span class="font-semibold tracking-wide">WebSocket SO Playground</span>
|
|
81
|
+
<span class="text-indigo-300 text-xs hidden sm:inline"
|
|
82
|
+
>@elliemae/pui-websocket-so · Phase II Service Orders</span
|
|
83
|
+
>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="flex items-center gap-4">
|
|
86
|
+
<span id="timer" class="text-xs text-indigo-200 tabular-nums hidden"
|
|
87
|
+
>00:00:00</span
|
|
88
|
+
>
|
|
89
|
+
<span id="stats" class="text-xs text-indigo-200 hidden">
|
|
90
|
+
<span title="Events dispatched"
|
|
91
|
+
>⚡ <span id="statEvents">0</span></span
|
|
92
|
+
>
|
|
93
|
+
<span class="ml-2" title="Active subscriptions across all guests"
|
|
94
|
+
>⬡ <span id="statSubs">0</span></span
|
|
95
|
+
>
|
|
96
|
+
</span>
|
|
97
|
+
<span
|
|
98
|
+
id="statusPill"
|
|
99
|
+
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"
|
|
100
|
+
>
|
|
101
|
+
● Disconnected
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
</header>
|
|
105
|
+
|
|
106
|
+
<!-- ════════════════════════ MAIN CONTENT ═════════════════════════ -->
|
|
107
|
+
<main class="flex flex-1 overflow-hidden">
|
|
108
|
+
<!-- ─────────────── LEFT PANEL ──────────────── -->
|
|
109
|
+
<aside
|
|
110
|
+
class="w-72 shrink-0 bg-white border-r border-gray-200 flex flex-col overflow-hidden"
|
|
111
|
+
>
|
|
112
|
+
<div class="scroll-panel flex-1 p-4 space-y-5">
|
|
113
|
+
<!-- § Server Details -->
|
|
114
|
+
<section>
|
|
115
|
+
<h2
|
|
116
|
+
class="text-xs font-semibold uppercase tracking-widest text-gray-400 mb-3"
|
|
117
|
+
>
|
|
118
|
+
Server Details
|
|
119
|
+
</h2>
|
|
120
|
+
|
|
121
|
+
<div class="mb-3">
|
|
122
|
+
<label
|
|
123
|
+
class="block text-xs font-medium text-gray-700 mb-1"
|
|
124
|
+
for="wsEnvironments"
|
|
125
|
+
>Environment</label
|
|
126
|
+
>
|
|
127
|
+
<select
|
|
128
|
+
id="wsEnvironments"
|
|
129
|
+
class="block w-full rounded border-gray-300 text-xs py-1.5 focus:ring-indigo-500 focus:border-indigo-500"
|
|
130
|
+
>
|
|
131
|
+
<option value="other" selected>Local (other)</option>
|
|
132
|
+
<option value="wss://dev.api.ellielabs.com">PSS-Dev</option>
|
|
133
|
+
<option value="wss://qa.api.ellielabs.com">PSS-Qa</option>
|
|
134
|
+
<option value="wss://int.api.ellielabs.com">PSS-Int</option>
|
|
135
|
+
<option value="wss://peg.api.ellielabs.com">PSS-Peg</option>
|
|
136
|
+
<option value="wss://stg.api.elliemae.com">PSS-Stg</option>
|
|
137
|
+
<option value="wss://api.elliemae.com">PSS-Prod</option>
|
|
138
|
+
</select>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div id="wsCustomDomainField" class="mb-3">
|
|
142
|
+
<label
|
|
143
|
+
class="block text-xs font-medium text-gray-700 mb-1"
|
|
144
|
+
for="wsCustomDomain"
|
|
145
|
+
>Server Domain</label
|
|
146
|
+
>
|
|
147
|
+
<input
|
|
148
|
+
type="text"
|
|
149
|
+
id="wsCustomDomain"
|
|
150
|
+
autocomplete="off"
|
|
151
|
+
value="ws://localhost:5000"
|
|
152
|
+
placeholder="ws://localhost:5000"
|
|
153
|
+
class="block w-full rounded border-gray-300 text-xs py-1.5 focus:ring-indigo-500 focus:border-indigo-500"
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div class="mb-3">
|
|
158
|
+
<label
|
|
159
|
+
class="block text-xs font-medium text-gray-700 mb-1"
|
|
160
|
+
for="wsPath"
|
|
161
|
+
>WS Path</label
|
|
162
|
+
>
|
|
163
|
+
<input
|
|
164
|
+
type="text"
|
|
165
|
+
id="wsPath"
|
|
166
|
+
autocomplete="off"
|
|
167
|
+
value="encompass/v1/stream"
|
|
168
|
+
placeholder="encompass/v1/stream"
|
|
169
|
+
class="block w-full rounded border-gray-300 text-xs py-1.5 focus:ring-indigo-500 focus:border-indigo-500"
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div class="mb-4">
|
|
174
|
+
<label
|
|
175
|
+
class="block text-xs font-medium text-gray-700 mb-1"
|
|
176
|
+
for="wsToken"
|
|
177
|
+
>Auth Token</label
|
|
178
|
+
>
|
|
179
|
+
<input
|
|
180
|
+
type="text"
|
|
181
|
+
id="wsToken"
|
|
182
|
+
autocomplete="off"
|
|
183
|
+
value="12345"
|
|
184
|
+
placeholder="Bearer token..."
|
|
185
|
+
class="block w-full rounded border-gray-300 text-xs py-1.5 font-mono focus:ring-indigo-500 focus:border-indigo-500"
|
|
186
|
+
/>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div class="flex gap-2">
|
|
190
|
+
<button
|
|
191
|
+
id="btnConnect"
|
|
192
|
+
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"
|
|
193
|
+
>
|
|
194
|
+
Connect
|
|
195
|
+
</button>
|
|
196
|
+
<button
|
|
197
|
+
id="btnDisconnect"
|
|
198
|
+
disabled
|
|
199
|
+
class="flex-1 rounded bg-gray-200 text-gray-400 text-xs font-medium py-1.5 cursor-not-allowed transition"
|
|
200
|
+
>
|
|
201
|
+
Disconnect
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div
|
|
206
|
+
id="connectError"
|
|
207
|
+
class="hidden mt-2 text-xs text-red-600 bg-red-50 border border-red-200 rounded p-2"
|
|
208
|
+
></div>
|
|
209
|
+
</section>
|
|
210
|
+
|
|
211
|
+
<!-- § Guest Panel -->
|
|
212
|
+
<section id="guestPanel" class="hidden">
|
|
213
|
+
<h2
|
|
214
|
+
class="text-xs font-semibold uppercase tracking-widest text-gray-400 mb-3"
|
|
215
|
+
>
|
|
216
|
+
Guest Simulator
|
|
217
|
+
</h2>
|
|
218
|
+
<div
|
|
219
|
+
id="guestStatus"
|
|
220
|
+
class="text-xs text-gray-500 bg-gray-50 rounded border border-gray-200 p-2"
|
|
221
|
+
>
|
|
222
|
+
Loading guest…
|
|
223
|
+
</div>
|
|
224
|
+
</section>
|
|
225
|
+
|
|
226
|
+
<!-- § About -->
|
|
227
|
+
<section class="text-xs text-gray-400 space-y-1 pt-2">
|
|
228
|
+
<p class="font-medium text-gray-500">How this works</p>
|
|
229
|
+
<p>
|
|
230
|
+
1. Click <strong>Connect</strong> to open the WebSocket and
|
|
231
|
+
register the SO with SSF Host.
|
|
232
|
+
</p>
|
|
233
|
+
<p>
|
|
234
|
+
2. The guest iframe loads automatically and connects to the host
|
|
235
|
+
via SSF Guest.
|
|
236
|
+
</p>
|
|
237
|
+
<p>3. Use the guest panel to subscribe and receive live events.</p>
|
|
238
|
+
</section>
|
|
239
|
+
</div>
|
|
240
|
+
</aside>
|
|
241
|
+
|
|
242
|
+
<!-- ──────────────── RIGHT AREA ─────────────── -->
|
|
243
|
+
<div class="flex-1 flex flex-col overflow-hidden">
|
|
244
|
+
<!-- ── Guest Simulator ── -->
|
|
245
|
+
<div
|
|
246
|
+
class="flex flex-col border-b border-gray-200"
|
|
247
|
+
style="height: 580px"
|
|
248
|
+
>
|
|
249
|
+
<div
|
|
250
|
+
class="flex items-center justify-between px-4 py-2 bg-white border-b border-gray-200 shrink-0"
|
|
251
|
+
>
|
|
252
|
+
<h2
|
|
253
|
+
class="text-xs font-semibold uppercase tracking-widest text-gray-400"
|
|
254
|
+
>
|
|
255
|
+
Guest Simulator
|
|
256
|
+
</h2>
|
|
257
|
+
<span id="guestFrameLabel" class="text-xs text-gray-400 italic"
|
|
258
|
+
>Connect to load guest</span
|
|
259
|
+
>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<!-- placeholder shown before connect -->
|
|
263
|
+
<div
|
|
264
|
+
id="guest-placeholder"
|
|
265
|
+
class="flex flex-col items-center justify-center flex-1 bg-gray-50 text-gray-400 gap-2"
|
|
266
|
+
>
|
|
267
|
+
<svg
|
|
268
|
+
class="w-10 h-10 text-gray-300"
|
|
269
|
+
fill="none"
|
|
270
|
+
stroke="currentColor"
|
|
271
|
+
viewBox="0 0 24 24"
|
|
272
|
+
>
|
|
273
|
+
<path
|
|
274
|
+
stroke-linecap="round"
|
|
275
|
+
stroke-linejoin="round"
|
|
276
|
+
stroke-width="1.5"
|
|
277
|
+
d="M9 17v-2a4 4 0 014-4h4m0 0l-3-3m3 3l-3 3M9 7H5a2 2 0 00-2 2v8a2 2 0 002 2h4"
|
|
278
|
+
/>
|
|
279
|
+
</svg>
|
|
280
|
+
<p class="text-sm">Connect to load the guest simulator</p>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<!-- iframe container – SSFHost.loadGuest() appends the iframe here -->
|
|
284
|
+
<div
|
|
285
|
+
id="guest-area"
|
|
286
|
+
class="hidden flex-1 overflow-hidden bg-white"
|
|
287
|
+
></div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<!-- ── Host Event Log ── -->
|
|
291
|
+
<div class="flex flex-col flex-1 overflow-hidden">
|
|
292
|
+
<div
|
|
293
|
+
class="flex items-center justify-between px-4 py-2 bg-white border-b border-gray-200 shrink-0"
|
|
294
|
+
>
|
|
295
|
+
<h2
|
|
296
|
+
class="text-xs font-semibold uppercase tracking-widest text-gray-400"
|
|
297
|
+
>
|
|
298
|
+
Host Event Log
|
|
299
|
+
</h2>
|
|
300
|
+
<button
|
|
301
|
+
id="btnClearLog"
|
|
302
|
+
class="text-xs text-gray-500 hover:text-red-500 transition"
|
|
303
|
+
>
|
|
304
|
+
Clear
|
|
305
|
+
</button>
|
|
306
|
+
</div>
|
|
307
|
+
<div
|
|
308
|
+
id="hostLog"
|
|
309
|
+
class="scroll-panel flex-1 p-3 bg-gray-950 font-mono"
|
|
310
|
+
>
|
|
311
|
+
<p class="text-gray-600 italic text-xs text-center py-4">
|
|
312
|
+
Events dispatched through the SO will appear here.
|
|
313
|
+
</p>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
</main>
|
|
318
|
+
|
|
319
|
+
<!-- ════════════════ CDN DEPENDENCIES ════════════════ -->
|
|
320
|
+
<!--
|
|
321
|
+
pui-diagnostics: sets window.emuiDiagnostics
|
|
322
|
+
ssf-host: sets window.ice.host.{SSFHost, ScriptingObject, Event, …}
|
|
323
|
+
-->
|
|
324
|
+
<script
|
|
325
|
+
src="https://assets.ellieservices.com/pui-diagnostics@3.2.0/umd/index.js"
|
|
326
|
+
async
|
|
327
|
+
></script>
|
|
328
|
+
<script src="https://cdn.mortgagetech.ice.com/ssf-host" async></script>
|
|
329
|
+
|
|
330
|
+
<!-- SO UMD is injected here by pui-cli start → window.emuiWebsocketSo.WebSocketSO -->
|
|
331
|
+
|
|
332
|
+
<!-- ════════════════════ PLAYGROUND SCRIPT ═════════════════════ -->
|
|
333
|
+
<script type="module">
|
|
334
|
+
/* ── Constants ──────────────────────────────────────────────── */
|
|
335
|
+
const GUEST_ID = 'wsso-guest-1';
|
|
336
|
+
const GUEST_URL = '/guest.html';
|
|
337
|
+
const ANALYTICS_MOCK = {
|
|
338
|
+
sendBAEvent: async () => {},
|
|
339
|
+
startTiming: async () => {},
|
|
340
|
+
endTiming: async () => {},
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/* ── State ──────────────────────────────────────────────────── */
|
|
344
|
+
let hostInstance = null;
|
|
345
|
+
let soInstance = null;
|
|
346
|
+
let connected = false;
|
|
347
|
+
let timerInterval = null;
|
|
348
|
+
let connectedAt = null;
|
|
349
|
+
let eventsDispatched = 0;
|
|
350
|
+
let activeSubs = 0;
|
|
351
|
+
|
|
352
|
+
/* ── UI refs ────────────────────────────────────────────────── */
|
|
353
|
+
const $pill = document.getElementById('statusPill');
|
|
354
|
+
const $timer = document.getElementById('timer');
|
|
355
|
+
const $stats = document.getElementById('stats');
|
|
356
|
+
const $statEvents = document.getElementById('statEvents');
|
|
357
|
+
const $statSubs = document.getElementById('statSubs');
|
|
358
|
+
const $btnConnect = document.getElementById('btnConnect');
|
|
359
|
+
const $btnDisconn = document.getElementById('btnDisconnect');
|
|
360
|
+
const $connectErr = document.getElementById('connectError');
|
|
361
|
+
const $guestPanel = document.getElementById('guestPanel');
|
|
362
|
+
const $guestStatus = document.getElementById('guestStatus');
|
|
363
|
+
const $guestLabel = document.getElementById('guestFrameLabel');
|
|
364
|
+
const $placeholder = document.getElementById('guest-placeholder');
|
|
365
|
+
const $guestArea = document.getElementById('guest-area');
|
|
366
|
+
const $hostLog = document.getElementById('hostLog');
|
|
367
|
+
const $clearBtn = document.getElementById('btnClearLog');
|
|
368
|
+
|
|
369
|
+
/* ── Environment picker ─────────────────────────────────────── */
|
|
370
|
+
const $env = document.getElementById('wsEnvironments');
|
|
371
|
+
const $domain = document.getElementById('wsCustomDomain');
|
|
372
|
+
const $path = document.getElementById('wsPath');
|
|
373
|
+
|
|
374
|
+
$env.addEventListener('change', () => {
|
|
375
|
+
const show = $env.value === 'other';
|
|
376
|
+
document.getElementById('wsCustomDomainField').style.display = show
|
|
377
|
+
? ''
|
|
378
|
+
: 'none';
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
function getWsUrl() {
|
|
382
|
+
const base =
|
|
383
|
+
$env.value === 'other'
|
|
384
|
+
? $domain.value.trim().replace(/\/$/, '')
|
|
385
|
+
: $env.value;
|
|
386
|
+
const path = $path.value.trim().replace(/^\//, '');
|
|
387
|
+
return `${base}/${path}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* ── Status pill ────────────────────────────────────────────── */
|
|
391
|
+
function setStatus(label, colour) {
|
|
392
|
+
const colours = {
|
|
393
|
+
gray: 'bg-gray-100 text-gray-600',
|
|
394
|
+
yellow: 'bg-yellow-100 text-yellow-700',
|
|
395
|
+
green: 'bg-green-100 text-green-700',
|
|
396
|
+
red: 'bg-red-100 text-red-700',
|
|
397
|
+
};
|
|
398
|
+
$pill.className = `${
|
|
399
|
+
colours[colour] ?? colours.gray
|
|
400
|
+
} inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium`;
|
|
401
|
+
$pill.textContent = `● ${label}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* ── Timer ──────────────────────────────────────────────────── */
|
|
405
|
+
function startTimer() {
|
|
406
|
+
connectedAt = Date.now();
|
|
407
|
+
$timer.classList.remove('hidden');
|
|
408
|
+
$stats.classList.remove('hidden');
|
|
409
|
+
timerInterval = setInterval(() => {
|
|
410
|
+
const s = Math.floor((Date.now() - connectedAt) / 1000);
|
|
411
|
+
const hh = String(Math.floor(s / 3600)).padStart(2, '0');
|
|
412
|
+
const mm = String(Math.floor((s % 3600) / 60)).padStart(2, '0');
|
|
413
|
+
const ss = String(s % 60).padStart(2, '0');
|
|
414
|
+
$timer.textContent = `${hh}:${mm}:${ss}`;
|
|
415
|
+
}, 1000);
|
|
416
|
+
}
|
|
417
|
+
function stopTimer() {
|
|
418
|
+
clearInterval(timerInterval);
|
|
419
|
+
$timer.classList.add('hidden');
|
|
420
|
+
$stats.classList.add('hidden');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* ── Dependency loader ──────────────────────────────────────── */
|
|
424
|
+
function waitForGlobals(paths, timeout = 15_000) {
|
|
425
|
+
return new Promise((resolve, reject) => {
|
|
426
|
+
const deadline = Date.now() + timeout;
|
|
427
|
+
const check = setInterval(() => {
|
|
428
|
+
const allReady = paths.every(
|
|
429
|
+
(p) =>
|
|
430
|
+
p.split('.').reduce((obj, k) => obj?.[k], window) !== undefined,
|
|
431
|
+
);
|
|
432
|
+
if (allReady) {
|
|
433
|
+
clearInterval(check);
|
|
434
|
+
resolve();
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (Date.now() > deadline) {
|
|
438
|
+
clearInterval(check);
|
|
439
|
+
reject(new Error(`Timed out waiting for: ${paths.join(', ')}`));
|
|
440
|
+
}
|
|
441
|
+
}, 50);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* ── Logger factory ─────────────────────────────────────────── */
|
|
446
|
+
function buildLogger(tag = 'WebSocketSO') {
|
|
447
|
+
const prefix = `[${tag}]`;
|
|
448
|
+
if (window.emuiDiagnostics) {
|
|
449
|
+
const { logger, http } = window.emuiDiagnostics;
|
|
450
|
+
try {
|
|
451
|
+
return logger({
|
|
452
|
+
transport: http(
|
|
453
|
+
'https://int.api.ellielabs.com/diagnostics/v2/logging',
|
|
454
|
+
),
|
|
455
|
+
index: 'wsso-playground',
|
|
456
|
+
team: 'ui platform',
|
|
457
|
+
appName: tag,
|
|
458
|
+
});
|
|
459
|
+
} catch {
|
|
460
|
+
/* fall through */
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// Console fallback
|
|
464
|
+
return {
|
|
465
|
+
debug: (m, ...a) => console.debug(prefix, m, ...a),
|
|
466
|
+
info: (m, ...a) => console.info(prefix, m, ...a),
|
|
467
|
+
warn: (m, ...a) => console.warn(prefix, m, ...a),
|
|
468
|
+
error: (m, ...a) => console.error(prefix, m, ...a),
|
|
469
|
+
audit: (m, ...a) => console.info(prefix, '[AUDIT]', m, ...a),
|
|
470
|
+
setLogLevel: () => {},
|
|
471
|
+
getLogLevel: () => 'debug',
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* ── Host log ───────────────────────────────────────────────── */
|
|
476
|
+
function syntaxHighlight(obj) {
|
|
477
|
+
const json = JSON.stringify(obj, null, 2);
|
|
478
|
+
return json.replace(
|
|
479
|
+
/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(?:\s*:)?|\b(true|false|null)\b|-?\d+\.?\d*(?:[eE][+-]?\d+)?)/g,
|
|
480
|
+
(m) => {
|
|
481
|
+
if (/^"/.test(m))
|
|
482
|
+
return /:$/.test(m)
|
|
483
|
+
? `<span class="jk">${m}</span>`
|
|
484
|
+
: `<span class="js">${m}</span>`;
|
|
485
|
+
if (/true|false/.test(m)) return `<span class="jb">${m}</span>`;
|
|
486
|
+
if (/null/.test(m)) return `<span class="jz">${m}</span>`;
|
|
487
|
+
return `<span class="jn">${m}</span>`;
|
|
488
|
+
},
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function appendLog(type, label, data, cssClass = 'log-info') {
|
|
493
|
+
const wasAtBottom =
|
|
494
|
+
$hostLog.scrollHeight - $hostLog.scrollTop <=
|
|
495
|
+
$hostLog.clientHeight + 4;
|
|
496
|
+
const ts = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
497
|
+
|
|
498
|
+
const row = document.createElement('div');
|
|
499
|
+
row.className = `${cssClass} mb-2 pl-3 py-1 rounded-sm`;
|
|
500
|
+
|
|
501
|
+
const head = document.createElement('div');
|
|
502
|
+
head.className = 'flex items-center gap-2 mb-0.5';
|
|
503
|
+
head.innerHTML = `
|
|
504
|
+
<span class="text-gray-500 text-xs tabular-nums">${ts}</span>
|
|
505
|
+
<span class="text-gray-300 text-xs font-medium">${label}</span>`;
|
|
506
|
+
row.appendChild(head);
|
|
507
|
+
|
|
508
|
+
if (data !== undefined) {
|
|
509
|
+
const pre = document.createElement('pre');
|
|
510
|
+
pre.className = 'text-gray-300';
|
|
511
|
+
pre.innerHTML =
|
|
512
|
+
typeof data === 'object'
|
|
513
|
+
? syntaxHighlight(data)
|
|
514
|
+
: `<span class="js">${String(data)}</span>`;
|
|
515
|
+
row.appendChild(pre);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Replace initial placeholder
|
|
519
|
+
const placeholder = $hostLog.querySelector('p.italic');
|
|
520
|
+
if (placeholder) placeholder.remove();
|
|
521
|
+
|
|
522
|
+
$hostLog.appendChild(row);
|
|
523
|
+
if (wasAtBottom) $hostLog.scrollTop = $hostLog.scrollHeight;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
$clearBtn.addEventListener('click', () => {
|
|
527
|
+
$hostLog.innerHTML =
|
|
528
|
+
'<p class="text-gray-600 italic text-xs text-center py-4">Cleared.</p>';
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
/* ── Guest area toggle ──────────────────────────────────────── */
|
|
532
|
+
function showGuestFrame() {
|
|
533
|
+
$placeholder.classList.add('hidden');
|
|
534
|
+
$guestArea.classList.remove('hidden');
|
|
535
|
+
$guestLabel.textContent = `Guest ID: ${GUEST_ID}`;
|
|
536
|
+
$guestPanel.classList.remove('hidden');
|
|
537
|
+
$guestStatus.textContent = 'Loading…';
|
|
538
|
+
}
|
|
539
|
+
function showGuestPlaceholder() {
|
|
540
|
+
$guestArea.classList.add('hidden');
|
|
541
|
+
$guestArea.innerHTML = '';
|
|
542
|
+
$placeholder.classList.remove('hidden');
|
|
543
|
+
$guestLabel.textContent = 'Connect to load guest';
|
|
544
|
+
$guestPanel.classList.add('hidden');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/* ── Dispatch-event interceptor ─────────────────────────────── */
|
|
548
|
+
/**
|
|
549
|
+
* Wraps the SSFHost so we can observe every dispatchEvent call
|
|
550
|
+
* and log it in the host log.
|
|
551
|
+
*/
|
|
552
|
+
function patchHostDispatch(host) {
|
|
553
|
+
const original = host.dispatchEvent.bind(host);
|
|
554
|
+
host.dispatchEvent = async (params) => {
|
|
555
|
+
const { event, eventParams, eventOptions } = params;
|
|
556
|
+
eventsDispatched++;
|
|
557
|
+
$statEvents.textContent = String(eventsDispatched);
|
|
558
|
+
|
|
559
|
+
appendLog(
|
|
560
|
+
'event',
|
|
561
|
+
`⚡ dispatch → guest "${eventOptions?.guestId ?? '*'}"`,
|
|
562
|
+
{
|
|
563
|
+
event,
|
|
564
|
+
eventParams,
|
|
565
|
+
eventOptions,
|
|
566
|
+
},
|
|
567
|
+
'log-event',
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
return original(params);
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/* ── Connect ────────────────────────────────────────────────── */
|
|
575
|
+
$btnConnect.addEventListener('click', async () => {
|
|
576
|
+
$connectErr.classList.add('hidden');
|
|
577
|
+
$btnConnect.disabled = true;
|
|
578
|
+
setStatus('Connecting…', 'yellow');
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
await waitForGlobals([
|
|
582
|
+
'emuiWebsocketSo.WebSocketSO',
|
|
583
|
+
'ice.host.SSFHost',
|
|
584
|
+
]);
|
|
585
|
+
} catch (e) {
|
|
586
|
+
showError(`CDN not loaded: ${e.message}`);
|
|
587
|
+
$btnConnect.disabled = false;
|
|
588
|
+
setStatus('Disconnected', 'gray');
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const url = getWsUrl();
|
|
593
|
+
const token = document.getElementById('wsToken').value.trim();
|
|
594
|
+
const logger = buildLogger('WebSocketSO');
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
// Create SSFHost once; reuse on subsequent connects
|
|
598
|
+
if (!hostInstance) {
|
|
599
|
+
hostInstance = new ice.host.SSFHost('wsso-playground', {
|
|
600
|
+
logger,
|
|
601
|
+
analyticsObj: ANALYTICS_MOCK,
|
|
602
|
+
});
|
|
603
|
+
patchHostDispatch(hostInstance);
|
|
604
|
+
appendLog('info', '🏠 SSFHost created', {
|
|
605
|
+
hostId: 'wsso-playground',
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Remove stale SO from previous session (if any)
|
|
610
|
+
try {
|
|
611
|
+
hostInstance.removeScriptingObject('websocket');
|
|
612
|
+
} catch {
|
|
613
|
+
/* none registered */
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Create the WebSocket Scripting Object
|
|
617
|
+
soInstance = new emuiWebsocketSo.WebSocketSO({
|
|
618
|
+
logger,
|
|
619
|
+
host: hostInstance,
|
|
620
|
+
url,
|
|
621
|
+
token,
|
|
622
|
+
onError: (err) => {
|
|
623
|
+
appendLog(
|
|
624
|
+
'error',
|
|
625
|
+
'🔴 SO error',
|
|
626
|
+
{ message: err.message },
|
|
627
|
+
'log-error',
|
|
628
|
+
);
|
|
629
|
+
setStatus('Error', 'red');
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
appendLog('info', '⚙️ WebSocketSO created', { url, token: '***' });
|
|
633
|
+
|
|
634
|
+
// Register SO with SSFHost
|
|
635
|
+
hostInstance.addScriptingObject(soInstance);
|
|
636
|
+
appendLog('info', '📋 SO registered with SSFHost as "websocket"');
|
|
637
|
+
|
|
638
|
+
// Open the WebSocket connection
|
|
639
|
+
await soInstance.open();
|
|
640
|
+
appendLog('open', '🟢 WebSocket opened', { url }, 'log-open');
|
|
641
|
+
|
|
642
|
+
connected = true;
|
|
643
|
+
setStatus('Connected', 'green');
|
|
644
|
+
startTimer();
|
|
645
|
+
eventsDispatched = 0;
|
|
646
|
+
$statEvents.textContent = '0';
|
|
647
|
+
$statSubs.textContent = '0';
|
|
648
|
+
|
|
649
|
+
$btnConnect.disabled = false;
|
|
650
|
+
$btnConnect.classList.add('opacity-50', 'cursor-not-allowed');
|
|
651
|
+
$btnConnect.disabled = true;
|
|
652
|
+
$btnDisconn.disabled = false;
|
|
653
|
+
$btnDisconn.classList.remove(
|
|
654
|
+
'bg-gray-200',
|
|
655
|
+
'text-gray-400',
|
|
656
|
+
'cursor-not-allowed',
|
|
657
|
+
);
|
|
658
|
+
$btnDisconn.classList.add(
|
|
659
|
+
'bg-red-600',
|
|
660
|
+
'text-white',
|
|
661
|
+
'hover:bg-red-700',
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
// Show guest area and load the guest iframe
|
|
665
|
+
showGuestFrame();
|
|
666
|
+
hostInstance.loadGuest({
|
|
667
|
+
id: GUEST_ID,
|
|
668
|
+
url: GUEST_URL,
|
|
669
|
+
title: 'Guest Simulator',
|
|
670
|
+
targetElement: $guestArea,
|
|
671
|
+
});
|
|
672
|
+
appendLog('info', `👤 loadGuest("${GUEST_ID}")`, { url: GUEST_URL });
|
|
673
|
+
$guestStatus.textContent = 'Guest loaded. Connecting via SSF…';
|
|
674
|
+
} catch (err) {
|
|
675
|
+
showError(err.message ?? String(err));
|
|
676
|
+
setStatus('Error', 'red');
|
|
677
|
+
$btnConnect.disabled = false;
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
/* ── Disconnect ─────────────────────────────────────────────── */
|
|
682
|
+
$btnDisconn.addEventListener('click', async () => {
|
|
683
|
+
$btnDisconn.disabled = true;
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
hostInstance.unloadGuest(GUEST_ID);
|
|
687
|
+
} catch {
|
|
688
|
+
/* ignore */
|
|
689
|
+
}
|
|
690
|
+
showGuestPlaceholder();
|
|
691
|
+
|
|
692
|
+
if (soInstance) {
|
|
693
|
+
try {
|
|
694
|
+
soInstance.close();
|
|
695
|
+
appendLog('close', '🔴 WebSocket closed', undefined, 'log-close');
|
|
696
|
+
} catch {
|
|
697
|
+
/* ignore */
|
|
698
|
+
}
|
|
699
|
+
try {
|
|
700
|
+
hostInstance.removeScriptingObject('websocket');
|
|
701
|
+
} catch {
|
|
702
|
+
/* ignore */
|
|
703
|
+
}
|
|
704
|
+
soInstance = null;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
connected = false;
|
|
708
|
+
setStatus('Disconnected', 'gray');
|
|
709
|
+
stopTimer();
|
|
710
|
+
|
|
711
|
+
$btnConnect.disabled = false;
|
|
712
|
+
$btnConnect.classList.remove('opacity-50', 'cursor-not-allowed');
|
|
713
|
+
$btnDisconn.disabled = true;
|
|
714
|
+
$btnDisconn.classList.add(
|
|
715
|
+
'bg-gray-200',
|
|
716
|
+
'text-gray-400',
|
|
717
|
+
'cursor-not-allowed',
|
|
718
|
+
);
|
|
719
|
+
$btnDisconn.classList.remove(
|
|
720
|
+
'bg-red-600',
|
|
721
|
+
'text-white',
|
|
722
|
+
'hover:bg-red-700',
|
|
723
|
+
);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
/* ── Error display ──────────────────────────────────────────── */
|
|
727
|
+
function showError(msg) {
|
|
728
|
+
$connectErr.textContent = msg;
|
|
729
|
+
$connectErr.classList.remove('hidden');
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/* ── Listen for guest status messages ───────────────────────── */
|
|
733
|
+
window.addEventListener('message', (ev) => {
|
|
734
|
+
if (ev.data?.__soPlayground !== true) return;
|
|
735
|
+
const { type, payload } = ev.data;
|
|
736
|
+
|
|
737
|
+
if (type === 'guest-ready') {
|
|
738
|
+
$guestStatus.innerHTML =
|
|
739
|
+
'<span class="text-green-600 font-medium">✓ Guest connected and SO proxy retrieved</span>';
|
|
740
|
+
appendLog('info', '👤 Guest ready', { guestId: GUEST_ID }, 'log-sub');
|
|
741
|
+
}
|
|
742
|
+
if (type === 'subscribe-ack') {
|
|
743
|
+
activeSubs++;
|
|
744
|
+
$statSubs.textContent = String(activeSubs);
|
|
745
|
+
appendLog('sub', '✅ Guest subscribed', payload, 'log-sub');
|
|
746
|
+
}
|
|
747
|
+
if (type === 'unsubscribe-ack') {
|
|
748
|
+
activeSubs = Math.max(0, activeSubs - 1);
|
|
749
|
+
$statSubs.textContent = String(activeSubs);
|
|
750
|
+
appendLog('unsub', '🗑️ Guest unsubscribed', payload, 'log-unsub');
|
|
751
|
+
}
|
|
752
|
+
if (type === 'guest-error') {
|
|
753
|
+
appendLog('error', '❌ Guest error', payload, 'log-error');
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
</script>
|
|
757
|
+
</body>
|
|
758
|
+
</html>
|