1/** 2 * WebchatService (Cognigy Webchat v3) 3 * 4 * Strict, LWC-safe wrapper targeting Cognigy Webchat v3. 5 * - Loads bundle (script tag or provided loader) 6 * - Initializes chat into a container (no auth at init) 7 * - Injects per-message token into data: sendMessage(text, data, options) 8 * - Optional, controlled monkey-patch of SDK client's sendMessage 9 * 10 * Principles: minimal surface, deterministic behavior, explicit errors, small methods. 11 */ 12 13export class WebchatService { 14 /** 15 * @param {Object} opts 16 * @param {string} opts.scriptUrl - URL to the Cognigy v3 webchat bundle (required) 17 * @param {string} [opts.globalInitName='initWebchat'] - global init function name 18 * @param {(ev:object)=>void} [opts.emit] - optional event emitter callback 19 * @param {(url:string)=>Promise<any>} [opts.loadScriptFn] - optional loader (for LWC loadScript) 20 */ 21 constructor({ scriptUrl, globalInitName = 'initWebchat', emit = null, loadScriptFn = null } = {}) { 22 if (!scriptUrl) throw new Error('WebchatService requires scriptUrl'); 23 24 this.scriptUrl = scriptUrl; 25 this.globalInitName = globalInitName; 26 this.emit = typeof emit === 'function' ? emit : () => {}; 27 this.loadScriptFn = loadScriptFn || this._defaultLoadScript.bind(this); 28 29 this.bundleLoaded = false; 30 this.instance = null; 31 32 // token state: undefined = never fetched, null = fetched & no token, string = token 33 this._previousToken = undefined; 34 35 // patch bookkeeping 36 this._patchedClient = null; 37 this._origClientSend = null; 38 39 // Default sendMessage warns if called before init 40 this.sendMessage = this._uninitializedSendMessage.bind(this); 41 } 42 43 /* =========================== 44 Loader 45 =========================== */ 46 47 /** 48 * Default script loader using a script tag. 49 * @private 50 */ 51 _defaultLoadScript(url) { 52 return new Promise((resolve, reject) => { 53 if (window[this.globalInitName]) return resolve(window[this.globalInitName]); 54 55 const existing = Array.from(document.getElementsByTagName('script')).find(s => s.src === url); 56 if (existing) { 57 existing.addEventListener('load', () => resolve(window[this.globalInitName])); 58 existing.addEventListener('error', (e) => reject(e)); 59 return; 60 } 61 62 const s = document.createElement('script'); 63 s.src = url; 64 s.async = true; 65 s.onload = () => resolve(window[this.globalInitName]); 66 s.onerror = () => reject(new Error(`Failed to load ${url}`)); 67 document.head.appendChild(s); 68 }); 69 } 70 71 /** 72 * Load the webchat bundle (idempotent). 73 */ 74 async loadBundle() { 75 if (this.bundleLoaded) return; 76 await this.loadScriptFn(this.scriptUrl); 77 this.bundleLoaded = true; 78 } 79 80 /* =========================== 81 Init 82 =========================== */ 83 84 /** 85 * Initialize webchat into a DOM container. 86 * No authentication is passed at init â auth happens per-message via token injection. 87 * 88 * @param {Object} cfg 89 * @param {Element} cfg.container - Required; use this.template.querySelector in LWC. 90 * @param {Object} [cfg.context={}] - Optional context passed to webchat. 91 * @param {string|null} [cfg.endpoint=null] - Optional Cognigy endpoint URL. 92 */ 93 async init({ container, context = {}, endpoint = null } = {}) { 94 if (!container) throw new Error('WebchatService.init requires a container DOM element'); 95 96 await this.loadBundle(); 97 98 const cfg = { 99 ...(endpoint ? { endpoint } : {}), 100 data: context || {}, 101 ui: { container } 102 }; 103 104 if (typeof window[this.globalInitName] === 'function') { 105 this.instance = await this._invokeInitFn(endpoint, cfg); 106 } else { 107 console.warn(`[WebchatService] ${this.globalInitName} not found after loading bundle`); 108 this.instance = { client: null, cfg }; 109 } 110 111 this._resetSendMessageToDefault();
112 this.emit({ type: 'webchat-initialized' }); 113 } 114 115 /** 116 * Tries the two-arg signature first, falls back to single-arg. 117 * Cognigy v3 uses (endpoint, config) but some bundles accept (config) only. 118 * @private 119 */ 120 async _invokeInitFn(endpoint, cfg) { 121 try { 122 return await window[this.globalInitName](endpoint, cfg); 123 } catch { 124 try { 125 return await window[this.globalInitName](cfg); 126 } catch (e) { 127 console.warn('[WebchatService] initWebchat failed; storing cfg for debugging', e); 128 return { client: null, cfg }; 129 } 130 } 131 } 132 133 /* =========================== 134 SDK Accessor & Default Forwarder 135 =========================== */ 136 137 /** 138 * Returns the SDK client. Some bundles expose it at `.client`, others are the client. 139 * @private 140 */ 141 _sdkClient() { 142 if (!this.instance) return null; 143 return this.instance.client || this.instance; 144 } 145 146 /** 147 * Safe placeholder used before init() is called. 148 * @private 149 */ 150 _uninitializedSendMessage() { 151 const err = new Error('[WebchatService] sendMessage called before init()'); 152 this.emit({ type: 'webchat-send-error', error: err }); 153 throw err; 154 } 155 156 /** 157 * Resets sendMessage to a direct forwarder to the SDK client. 158 * @private 159 */ 160 _resetSendMessageToDefault() { 161 this.sendMessage = async (text, data, options) => { 162 const client = this._sdkClient(); 163 if (!client || typeof client.sendMessage !== 'function') { 164 const err = new Error('Webchat client not available or sendMessage missing'); 165 this.emit({ type: 'webchat-send-error', error: err }); 166 throw err; 167 } 168 return client.sendMessage.call(client, text, data, options); 169 }; 170 } 171 172 /* =========================== 173 Token Injection 174 =========================== */ 175 176 /** 177 * Attach a TokenService so that a fresh token is injected into every outgoing message. 178 * 179 * @param {Object} tokenService - Must implement fetchWebchatToken(forceRefresh:boolean):Promise<string|null>. 180 * @param {Object} [opts] 181 * @param {string} [opts.fieldPath='_meta.token'] - Dot-path inside data to place the token. 182 * @param {boolean} [opts.patchSdkIfNeeded=false] - If true, also patches SDK client's sendMessage. 183 */ 184 setTokenInjection(tokenService, { fieldPath = '_meta.token', patchSdkIfNeeded = false } = {}) { 185 if (!tokenService || typeof tokenService.fetchWebchatToken !== 'function') { 186 throw new Error('tokenService must implement fetchWebchatToken(forceRefresh:boolean):Promise<string|null>'); 187 } 188 189 this.sendMessage = async (text, data = undefined, options = undefined) => { 190 if (typeof text !== 'string') { 191 const err = new Error('sendMessage requires first argument to be a string'); 192 this.emit({ type: 'webchat-send-error', error: err }); 193 throw err; 194 } 195 196 data = this._ensureDataObject(data); 197 data = await this._fetchAndInjectToken(data, tokenService, fieldPath); 198 199 const client = this._sdkClient(); 200 if (!client || typeof client.sendMessage !== 'function') { 201 const err = new Error('Webchat client not available or sendMessage missing'); 202 this.emit({ type: 'webchat-send-error', error: err }); 203 throw err; 204 } 205 206 return client.sendMessage.call(client, text, data, options); 207 }; 208 209 if (patchSdkIfNeeded) { 210 try { 211 this._patchClientSendMessage(tokenService, { fieldPath }); 212 } catch (err) { 213 console.warn('[WebchatService] failed to patch SDK sendMessage', err); 214 } 215 } 216 } 217 218 /** 219 * Fetches a token from the service and injects it into the data object at fieldPath. 220 * Updates internal token state for smart force-refresh logic. 221 * @private 222 */ 223 async _fetchAndInjectToken(data, tokenService, fieldPath) { 224 const shouldForce = this._previousToken === null; 225 let token = null; 226 try { 227 token = await tokenService.fetchWebchatToken(shouldForce); 228 } catch (err) {
229 this.emit({ type: 'token-fetch-error', error: err }); 230 } 231 this._previousToken = token; 232 this._setAtPath(data, fieldPath, token); 233 return data; 234 } 235 236 /** 237 * Ensures `data` is a plain object, making a shallow copy to avoid mutating caller's data. 238 * @private 239 */ 240 _ensureDataObject(data) { 241 if (data == null || typeof data !== 'object') return {}; 242 return { ...data }; 243 } 244 245 /** 246 * Sets a value at a dot-notation path on an object, creating intermediate objects as needed. 247 * @private 248 */ 249 _setAtPath(obj, path, value) { 250 const parts = path.split('.'); 251 let cur = obj; 252 for (let i = 0; i < parts.length - 1; i++) { 253 const p = parts[i]; 254 if (!Object.prototype.hasOwnProperty.call(cur, p) || typeof cur[p] !== 'object') { 255 cur[p] = {}; 256 } 257 cur = cur[p]; 258 } 259 cur[parts[parts.length - 1]] = value; 260 } 261 262 /* =========================== 263 Optional Controlled Monkey-Patch 264 =========================== */ 265 266 /** 267 * Monkey-patches the SDK client's sendMessage to inject tokens at the SDK level. 268 * Use when the Cognigy SDK sends messages internally (e.g. from UI buttons) bypassing our wrapper. 269 * @private 270 */ 271 _patchClientSendMessage(tokenService, { fieldPath = '_meta.token' } = {}) { 272 const client = this._sdkClient(); 273 if (!client || typeof client.sendMessage !== 'function') { 274 throw new Error('client.sendMessage not available to patch'); 275 } 276 if (client._sendMessagePatched) return; 277 278 client._origSendMessage = client.sendMessage; 279 const self = this; 280 281 client.sendMessage = async function patchedSendMessage(text, data = undefined, options = undefined) { 282 if (typeof text !== 'string') { 283 return client._origSendMessage.apply(client, [text, data, options]); 284 } 285 286 data = self._ensureDataObject(data); 287 data = await self._fetchAndInjectToken(data, tokenService, fieldPath); 288 289 try { 290 return await client._origSendMessage.call(client, text, data, options); 291 } catch (err) { 292 self.emit({ type: 'webchat-send-error', error: err }); 293 throw err; 294 } 295 }; 296 297 client._sendMessagePatched = true; 298 this._patchedClient = client; 299 this._origClientSend = client._origSendMessage; 300 } 301 302 /* =========================== 303 Destroy 304 =========================== */ 305 306 /** 307 * Tears down the webchat instance and restores any patched SDK methods. 308 */ 309 async destroy() { 310 try { 311 this._restoreClientSendMessage(); 312 await this._destroyInstance(); 313 } finally { 314 this.instance = null; 315 this.bundleLoaded = false; 316 this._patchedClient = null; 317 this._origClientSend = null; 318 this._previousToken = undefined; 319 this.sendMessage = this._uninitializedSendMessage.bind(this); 320 this.emit({ type: 'webchat-destroyed' }); 321 } 322 } 323 324 /** @private */ 325 _restoreClientSendMessage() { 326 if (!this._patchedClient || !this._origClientSend) return; 327 try { 328 this._patchedClient.sendMessage = this._origClientSend; 329 delete this._patchedClient._origSendMessage; 330 delete this._patchedClient._sendMessagePatched; 331 } catch (_) { /* ignore */ } 332 } 333 334 /** @private */ 335 async _destroyInstance() { 336 if (this.instance && typeof this.instance.destroy === 'function') { 337 try { 338 await this.instance.destroy(); 339 } catch (_) { /* ignore */ } 340 } 341 } 342}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.