@cendarsoss/pusher-js 8.4.11

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 (425) hide show
  1. package/.editorconfig +14 -0
  2. package/.github/ISSUE_TEMPLATE.md +11 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
  4. package/.github/dependabot.yml +14 -0
  5. package/.github/stale.yml +26 -0
  6. package/.github/workflows/release.yml +112 -0
  7. package/.github/workflows/release_pr.yml +43 -0
  8. package/.github/workflows/run-tests.yml +62 -0
  9. package/.gitmodules +3 -0
  10. package/.prettierrc +2 -0
  11. package/CHANGELOG.md +928 -0
  12. package/DELTA_COMPRESSION.md +365 -0
  13. package/DELTA_USAGE.md +179 -0
  14. package/IMPLEMENTATION_SUMMARY.md +261 -0
  15. package/IMPORT_GUIDE.md +638 -0
  16. package/LIBRARY_STRUCTURE_ANALYSIS.md +940 -0
  17. package/LICENCE +19 -0
  18. package/Makefile +14 -0
  19. package/README.md +709 -0
  20. package/TAG_FILTERING_CLIENT.md +471 -0
  21. package/bower.json +19 -0
  22. package/bun.lock +2695 -0
  23. package/dist/node/filter.js +252 -0
  24. package/dist/node/filter.js.map +1 -0
  25. package/dist/node/pusher.js +4434 -0
  26. package/dist/node/pusher.js.map +1 -0
  27. package/dist/web/filter.mjs +252 -0
  28. package/dist/web/filter.mjs.map +1 -0
  29. package/dist/web/pusher.mjs +5889 -0
  30. package/dist/web/pusher.mjs.map +1 -0
  31. package/examples/delta-compression-example.html +372 -0
  32. package/examples/delta-seamless-example.html +185 -0
  33. package/index.d.ts +36 -0
  34. package/integration_tests_server/index.js +176 -0
  35. package/integration_tests_server/package-lock.json +1177 -0
  36. package/integration_tests_server/package.json +15 -0
  37. package/interactive/.env +16 -0
  38. package/interactive/CONFLATION_TEST.md +73 -0
  39. package/interactive/DELTA_COMPRESSION_TESTING.md +262 -0
  40. package/interactive/bun.lock +208 -0
  41. package/interactive/package-lock.json +1075 -0
  42. package/interactive/package.json +32 -0
  43. package/interactive/public/app.js +1363 -0
  44. package/interactive/public/bundle-entry.js +14 -0
  45. package/interactive/public/conflation-test.html +508 -0
  46. package/interactive/public/conflation-test.js +785 -0
  47. package/interactive/public/delta-compression.js +1090 -0
  48. package/interactive/public/dist/bundle.js +5857 -0
  49. package/interactive/public/index.html +392 -0
  50. package/interactive/public/main.js +20 -0
  51. package/interactive/public/style.css +823 -0
  52. package/interactive/server.js +246 -0
  53. package/interactive/test-bundle.html +89 -0
  54. package/interactive/test-delta.js +146 -0
  55. package/node.js +1 -0
  56. package/package.json +94 -0
  57. package/pusher-with-encryption/index.js +1 -0
  58. package/react-native/index.d.ts +29 -0
  59. package/react-native/index.js +1 -0
  60. package/spec/config/jasmine/helpers/reporter.js +14 -0
  61. package/spec/config/jasmine/integration.json +13 -0
  62. package/spec/config/jasmine/unit.json +13 -0
  63. package/spec/config/jasmine/webpack.integration.js +33 -0
  64. package/spec/config/jasmine/webpack.unit.js +30 -0
  65. package/spec/config/karma/available_browsers.json +4957 -0
  66. package/spec/config/karma/config.ci.js +25 -0
  67. package/spec/config/karma/config.common.js +50 -0
  68. package/spec/config/karma/config.integration.js +26 -0
  69. package/spec/config/karma/config.unit.js +10 -0
  70. package/spec/config/karma/config.worker.js +34 -0
  71. package/spec/config/karma/integration.js +24 -0
  72. package/spec/config/karma/unit.js +20 -0
  73. package/spec/javascripts/helpers/mocks.js +274 -0
  74. package/spec/javascripts/helpers/node/integration.js +33 -0
  75. package/spec/javascripts/helpers/node/mock-dom-dependencies.ts +1 -0
  76. package/spec/javascripts/helpers/pusher_integration.js +1 -0
  77. package/spec/javascripts/helpers/pusher_integration_class.ts +12 -0
  78. package/spec/javascripts/helpers/timers/promises.js +9 -0
  79. package/spec/javascripts/helpers/waitsFor.js +37 -0
  80. package/spec/javascripts/helpers/web/integration.js +44 -0
  81. package/spec/javascripts/helpers/worker/mock-dom-dependencies.js +1 -0
  82. package/spec/javascripts/integration/core/cluster_config_spec.js +153 -0
  83. package/spec/javascripts/integration/core/falling_back_spec.js +195 -0
  84. package/spec/javascripts/integration/core/pusher_spec/index.js +68 -0
  85. package/spec/javascripts/integration/core/pusher_spec/test_builder.js +715 -0
  86. package/spec/javascripts/integration/core/timeout_configuration_spec.js +200 -0
  87. package/spec/javascripts/integration/core/transport_lists_spec.js +103 -0
  88. package/spec/javascripts/integration/index.node.js +12 -0
  89. package/spec/javascripts/integration/index.web.js +63 -0
  90. package/spec/javascripts/integration/index.worker.js +13 -0
  91. package/spec/javascripts/integration/web/dom/jsonp_spec.js +97 -0
  92. package/spec/javascripts/integration/web/dom/script_request_spec.js +90 -0
  93. package/spec/javascripts/polyfills/index.js +105 -0
  94. package/spec/javascripts/unit/core/channels/channel_spec.js +355 -0
  95. package/spec/javascripts/unit/core/channels/channels_spec.js +94 -0
  96. package/spec/javascripts/unit/core/channels/encrypted_channel_spec.js +343 -0
  97. package/spec/javascripts/unit/core/channels/presence_channel_spec.js +553 -0
  98. package/spec/javascripts/unit/core/channels/private_channel_spec.js +182 -0
  99. package/spec/javascripts/unit/core/config_spec.js +507 -0
  100. package/spec/javascripts/unit/core/connection/connection_manager_spec.js +656 -0
  101. package/spec/javascripts/unit/core/connection/connection_spec.js +286 -0
  102. package/spec/javascripts/unit/core/connection/handshake_spec.js +160 -0
  103. package/spec/javascripts/unit/core/connection/protocol_spec.js +420 -0
  104. package/spec/javascripts/unit/core/defaults_spec.js +26 -0
  105. package/spec/javascripts/unit/core/events_dispatcher_spec.js +385 -0
  106. package/spec/javascripts/unit/core/http/http_polling_socket_spec.js +60 -0
  107. package/spec/javascripts/unit/core/http/http_request_spec.js +185 -0
  108. package/spec/javascripts/unit/core/http/http_socket_spec.js +370 -0
  109. package/spec/javascripts/unit/core/http/http_streaming_socket_spec.js +56 -0
  110. package/spec/javascripts/unit/core/http/http_xhr_request_spec.js +164 -0
  111. package/spec/javascripts/unit/core/logger_spec.js +133 -0
  112. package/spec/javascripts/unit/core/pusher_spec.js +613 -0
  113. package/spec/javascripts/unit/core/pusher_with_encryption_spec.js +18 -0
  114. package/spec/javascripts/unit/core/strategies/best_connected_ever_strategy_spec.js +104 -0
  115. package/spec/javascripts/unit/core/strategies/delayed_strategy_spec.js +95 -0
  116. package/spec/javascripts/unit/core/strategies/first_connected_strategy_spec.js +68 -0
  117. package/spec/javascripts/unit/core/strategies/if_strategy_spec.js +165 -0
  118. package/spec/javascripts/unit/core/strategies/sequential_strategy_spec.js +213 -0
  119. package/spec/javascripts/unit/core/strategies/transport_strategy_spec.js +250 -0
  120. package/spec/javascripts/unit/core/strategies/websocket_prioritized_cached_strategy_spec.js +400 -0
  121. package/spec/javascripts/unit/core/timeline/timeline_spec.js +153 -0
  122. package/spec/javascripts/unit/core/transports/assistant_to_the_transport_manager_spec.js +223 -0
  123. package/spec/javascripts/unit/core/transports/hosts_and_ports_spec.js +85 -0
  124. package/spec/javascripts/unit/core/transports/transport_connection_spec.js +585 -0
  125. package/spec/javascripts/unit/core/transports/transport_manager_spec.js +64 -0
  126. package/spec/javascripts/unit/core/user_spec.js +303 -0
  127. package/spec/javascripts/unit/core/utils/periodic_timer_spec.js +74 -0
  128. package/spec/javascripts/unit/core/utils/timers_spec.js +157 -0
  129. package/spec/javascripts/unit/core/utils/url_store_spec.js +14 -0
  130. package/spec/javascripts/unit/core/watchlist_spec.js +48 -0
  131. package/spec/javascripts/unit/core_with_runtime/auth/channel_authorizer_spec.js +137 -0
  132. package/spec/javascripts/unit/core_with_runtime/auth/deprecated_channel_authorizer_spec.js +48 -0
  133. package/spec/javascripts/unit/core_with_runtime/auth/user_authorizer_spec.js +128 -0
  134. package/spec/javascripts/unit/core_with_runtime/readme.md +5 -0
  135. package/spec/javascripts/unit/index.node.js +11 -0
  136. package/spec/javascripts/unit/index.web.js +12 -0
  137. package/spec/javascripts/unit/index.worker.js +11 -0
  138. package/spec/javascripts/unit/isomorphic/transports/hosts_and_ports_spec.js +82 -0
  139. package/spec/javascripts/unit/isomorphic/transports/transports_spec.js +202 -0
  140. package/spec/javascripts/unit/node/timeline_sender_spec.js +83 -0
  141. package/spec/javascripts/unit/web/dom/dependency_loader_spec.js +249 -0
  142. package/spec/javascripts/unit/web/dom/jsonp_request_spec.js +130 -0
  143. package/spec/javascripts/unit/web/dom/script_receiver_factory_spec.js +68 -0
  144. package/spec/javascripts/unit/web/http/http_xdomain_request_spec.js +222 -0
  145. package/spec/javascripts/unit/web/pusher_authorizer_spec.js +64 -0
  146. package/spec/javascripts/unit/web/timeline/timeline_sender_spec.js +131 -0
  147. package/spec/javascripts/unit/web/transports/hosts_and_ports_spec.js +127 -0
  148. package/spec/javascripts/unit/web/transports/transports_spec.js +444 -0
  149. package/spec/javascripts/unit/worker/channel_authorizer_spec.js +156 -0
  150. package/spec/javascripts/unit/worker/timeline_sender_spec.js +76 -0
  151. package/src/core/auth/auth_transports.ts +18 -0
  152. package/src/core/auth/channel_authorizer.ts +64 -0
  153. package/src/core/auth/deprecated_channel_authorizer.ts +56 -0
  154. package/src/core/auth/options.ts +76 -0
  155. package/src/core/auth/user_authenticator.ts +62 -0
  156. package/src/core/base64.ts +49 -0
  157. package/src/core/channels/channel.ts +173 -0
  158. package/src/core/channels/channel_table.ts +7 -0
  159. package/src/core/channels/channels.ts +86 -0
  160. package/src/core/channels/encrypted_channel.ts +150 -0
  161. package/src/core/channels/filter.ts +342 -0
  162. package/src/core/channels/members.ts +80 -0
  163. package/src/core/channels/metadata.ts +5 -0
  164. package/src/core/channels/presence_channel.ts +113 -0
  165. package/src/core/channels/private_channel.ts +25 -0
  166. package/src/core/config.ts +189 -0
  167. package/src/core/connection/callbacks.ts +21 -0
  168. package/src/core/connection/connection.ts +160 -0
  169. package/src/core/connection/connection_manager.ts +371 -0
  170. package/src/core/connection/connection_manager_options.ts +14 -0
  171. package/src/core/connection/handshake/handshake_payload.ts +10 -0
  172. package/src/core/connection/handshake/index.ts +90 -0
  173. package/src/core/connection/protocol/action.ts +8 -0
  174. package/src/core/connection/protocol/message-types.ts +11 -0
  175. package/src/core/connection/protocol/protocol.ts +166 -0
  176. package/src/core/defaults.ts +66 -0
  177. package/src/core/delta/channel_state.ts +194 -0
  178. package/src/core/delta/decoders.ts +129 -0
  179. package/src/core/delta/index.ts +10 -0
  180. package/src/core/delta/manager.ts +504 -0
  181. package/src/core/delta/types.ts +60 -0
  182. package/src/core/errors.ts +69 -0
  183. package/src/core/events/callback.ts +6 -0
  184. package/src/core/events/callback_registry.ts +75 -0
  185. package/src/core/events/callback_table.ts +7 -0
  186. package/src/core/events/dispatcher.ts +84 -0
  187. package/src/core/http/ajax.ts +24 -0
  188. package/src/core/http/http_factory.ts +16 -0
  189. package/src/core/http/http_polling_socket.ts +24 -0
  190. package/src/core/http/http_request.ts +81 -0
  191. package/src/core/http/http_socket.ts +220 -0
  192. package/src/core/http/http_streaming_socket.ts +19 -0
  193. package/src/core/http/request_hooks.ts +9 -0
  194. package/src/core/http/socket_hooks.ts +11 -0
  195. package/src/core/http/state.ts +7 -0
  196. package/src/core/http/url_location.ts +6 -0
  197. package/src/core/logger.ts +66 -0
  198. package/src/core/options.ts +61 -0
  199. package/src/core/pusher-licence.js +7 -0
  200. package/src/core/pusher-with-encryption.js +1 -0
  201. package/src/core/pusher-with-encryption.ts +14 -0
  202. package/src/core/pusher.js +10 -0
  203. package/src/core/pusher.ts +412 -0
  204. package/src/core/reachability.ts +7 -0
  205. package/src/core/socket.ts +14 -0
  206. package/src/core/strategies/best_connected_ever_strategy.ts +81 -0
  207. package/src/core/strategies/delayed_strategy.ts +48 -0
  208. package/src/core/strategies/first_connected_strategy.ts +31 -0
  209. package/src/core/strategies/if_strategy.ts +34 -0
  210. package/src/core/strategies/sequential_strategy.ts +129 -0
  211. package/src/core/strategies/strategy.ts +8 -0
  212. package/src/core/strategies/strategy_builder.ts +67 -0
  213. package/src/core/strategies/strategy_options.ts +18 -0
  214. package/src/core/strategies/strategy_runner.ts +6 -0
  215. package/src/core/strategies/transport_strategy.ts +144 -0
  216. package/src/core/strategies/websocket_prioritized_cached_strategy.ts +157 -0
  217. package/src/core/timeline/level.ts +7 -0
  218. package/src/core/timeline/timeline.ts +90 -0
  219. package/src/core/timeline/timeline_sender.ts +33 -0
  220. package/src/core/timeline/timeline_transport.ts +11 -0
  221. package/src/core/transports/assistant_to_the_transport_manager.ts +104 -0
  222. package/src/core/transports/ping_delay_options.ts +7 -0
  223. package/src/core/transports/transport.ts +54 -0
  224. package/src/core/transports/transport_connection.ts +241 -0
  225. package/src/core/transports/transport_connection_options.ts +8 -0
  226. package/src/core/transports/transport_hooks.ts +16 -0
  227. package/src/core/transports/transport_manager.ts +52 -0
  228. package/src/core/transports/transports_table.ts +12 -0
  229. package/src/core/transports/url_scheme.ts +13 -0
  230. package/src/core/transports/url_schemes.ts +47 -0
  231. package/src/core/user.ts +186 -0
  232. package/src/core/util.ts +34 -0
  233. package/src/core/utils/collections.ts +353 -0
  234. package/src/core/utils/factory.ts +79 -0
  235. package/src/core/utils/flat_promise.ts +10 -0
  236. package/src/core/utils/timers/abstract_timer.ts +39 -0
  237. package/src/core/utils/timers/index.ts +39 -0
  238. package/src/core/utils/timers/scheduling.ts +11 -0
  239. package/src/core/utils/timers/timed_callback.ts +5 -0
  240. package/src/core/utils/url_store.ts +48 -0
  241. package/src/core/watchlist.ts +31 -0
  242. package/src/d.ts/constants/index.d.ts +5 -0
  243. package/src/d.ts/faye-websocket/faye-websocket.d.ts +21 -0
  244. package/src/d.ts/global/global.d.ts +1 -0
  245. package/src/d.ts/module/module.d.ts +12 -0
  246. package/src/d.ts/tweetnacl-util/index.d.ts +6 -0
  247. package/src/d.ts/window/events.d.ts +4 -0
  248. package/src/d.ts/window/sockjs.d.ts +3 -0
  249. package/src/d.ts/window/websocket.d.ts +4 -0
  250. package/src/d.ts/window/xmlhttprequest.d.ts +3 -0
  251. package/src/filter.ts +5 -0
  252. package/src/index.ts +8 -0
  253. package/src/runtimes/interface.ts +60 -0
  254. package/src/runtimes/isomorphic/auth/xhr_auth.ts +90 -0
  255. package/src/runtimes/isomorphic/default_strategy.ts +155 -0
  256. package/src/runtimes/isomorphic/http/http.ts +32 -0
  257. package/src/runtimes/isomorphic/http/http_xhr_request.ts +35 -0
  258. package/src/runtimes/isomorphic/runtime.ts +62 -0
  259. package/src/runtimes/isomorphic/timeline/xhr_timeline.ts +50 -0
  260. package/src/runtimes/isomorphic/transports/transport_connection_initializer.ts +19 -0
  261. package/src/runtimes/isomorphic/transports/transports.ts +83 -0
  262. package/src/runtimes/node/net_info.ts +10 -0
  263. package/src/runtimes/node/runtime.ts +68 -0
  264. package/src/runtimes/react-native/net_info.ts +42 -0
  265. package/src/runtimes/react-native/runtime.ts +65 -0
  266. package/src/runtimes/web/auth/jsonp_auth.ts +51 -0
  267. package/src/runtimes/web/browser.ts +24 -0
  268. package/src/runtimes/web/default_strategy.ts +201 -0
  269. package/src/runtimes/web/dom/dependencies.ts +16 -0
  270. package/src/runtimes/web/dom/dependency_loader.ts +93 -0
  271. package/src/runtimes/web/dom/json2.js +486 -0
  272. package/src/runtimes/web/dom/jsonp_request.ts +52 -0
  273. package/src/runtimes/web/dom/script_receiver.ts +8 -0
  274. package/src/runtimes/web/dom/script_receiver_factory.ts +57 -0
  275. package/src/runtimes/web/dom/script_request.ts +85 -0
  276. package/src/runtimes/web/http/http.ts +8 -0
  277. package/src/runtimes/web/http/http_xdomain_request.ts +37 -0
  278. package/src/runtimes/web/net_info.ts +50 -0
  279. package/src/runtimes/web/runtime.ts +174 -0
  280. package/src/runtimes/web/timeline/jsonp_timeline.ts +34 -0
  281. package/src/runtimes/web/transports/transport_connection_initializer.ts +39 -0
  282. package/src/runtimes/web/transports/transports.ts +67 -0
  283. package/src/runtimes/worker/auth/fetch_auth.ts +69 -0
  284. package/src/runtimes/worker/net_info.ts +10 -0
  285. package/src/runtimes/worker/runtime.ts +75 -0
  286. package/src/runtimes/worker/timeline/fetch_timeline.ts +39 -0
  287. package/tsconfig.json +18 -0
  288. package/types/spec/javascripts/helpers/node/mock-dom-dependencies.d.ts +1 -0
  289. package/types/spec/javascripts/helpers/pusher_integration_class.d.ts +4 -0
  290. package/types/src/core/auth/auth_transports.d.ts +9 -0
  291. package/types/src/core/auth/channel_authorizer.d.ts +3 -0
  292. package/types/src/core/auth/deprecated_channel_authorizer.d.ts +18 -0
  293. package/types/src/core/auth/options.d.ts +48 -0
  294. package/types/src/core/auth/user_authenticator.d.ts +3 -0
  295. package/types/src/core/base64.d.ts +1 -0
  296. package/types/src/core/channels/channel.d.ts +25 -0
  297. package/types/src/core/channels/channel_table.d.ts +5 -0
  298. package/types/src/core/channels/channels.d.ts +12 -0
  299. package/types/src/core/channels/encrypted_channel.d.ts +15 -0
  300. package/types/src/core/channels/filter.d.ts +33 -0
  301. package/types/src/core/channels/members.d.ts +14 -0
  302. package/types/src/core/channels/metadata.d.ts +4 -0
  303. package/types/src/core/channels/presence_channel.d.ts +13 -0
  304. package/types/src/core/channels/private_channel.d.ts +5 -0
  305. package/types/src/core/config.d.ts +31 -0
  306. package/types/src/core/connection/callbacks.d.ts +18 -0
  307. package/types/src/core/connection/connection.d.ts +16 -0
  308. package/types/src/core/connection/connection_manager.d.ts +50 -0
  309. package/types/src/core/connection/connection_manager_options.d.ts +11 -0
  310. package/types/src/core/connection/handshake/handshake_payload.d.ts +8 -0
  311. package/types/src/core/connection/handshake/index.d.ts +12 -0
  312. package/types/src/core/connection/protocol/action.d.ts +7 -0
  313. package/types/src/core/connection/protocol/message-types.d.ts +10 -0
  314. package/types/src/core/connection/protocol/protocol.d.ts +10 -0
  315. package/types/src/core/defaults.d.ts +26 -0
  316. package/types/src/core/delta/channel_state.d.ts +23 -0
  317. package/types/src/core/delta/decoders.d.ts +12 -0
  318. package/types/src/core/delta/index.d.ts +4 -0
  319. package/types/src/core/delta/manager.d.ts +27 -0
  320. package/types/src/core/delta/types.d.ts +50 -0
  321. package/types/src/core/errors.d.ts +28 -0
  322. package/types/src/core/events/callback.d.ts +5 -0
  323. package/types/src/core/events/callback_registry.d.ts +11 -0
  324. package/types/src/core/events/callback_table.d.ts +5 -0
  325. package/types/src/core/events/dispatcher.d.ts +14 -0
  326. package/types/src/core/http/ajax.d.ts +16 -0
  327. package/types/src/core/http/http_factory.d.ts +13 -0
  328. package/types/src/core/http/http_polling_socket.d.ts +3 -0
  329. package/types/src/core/http/http_request.d.ts +17 -0
  330. package/types/src/core/http/http_socket.d.ts +32 -0
  331. package/types/src/core/http/http_streaming_socket.d.ts +3 -0
  332. package/types/src/core/http/request_hooks.d.ts +6 -0
  333. package/types/src/core/http/socket_hooks.d.ts +8 -0
  334. package/types/src/core/http/state.d.ts +6 -0
  335. package/types/src/core/http/url_location.d.ts +5 -0
  336. package/types/src/core/logger.d.ts +11 -0
  337. package/types/src/core/options.d.ts +36 -0
  338. package/types/src/core/pusher-with-encryption.d.ts +5 -0
  339. package/types/src/core/pusher.d.ts +56 -0
  340. package/types/src/core/reachability.d.ts +5 -0
  341. package/types/src/core/socket.d.ts +12 -0
  342. package/types/src/core/strategies/best_connected_ever_strategy.d.ts +10 -0
  343. package/types/src/core/strategies/delayed_strategy.d.ts +15 -0
  344. package/types/src/core/strategies/first_connected_strategy.d.ts +8 -0
  345. package/types/src/core/strategies/if_strategy.d.ts +10 -0
  346. package/types/src/core/strategies/sequential_strategy.d.ts +16 -0
  347. package/types/src/core/strategies/strategy.d.ts +6 -0
  348. package/types/src/core/strategies/strategy_builder.d.ts +5 -0
  349. package/types/src/core/strategies/strategy_options.d.ts +16 -0
  350. package/types/src/core/strategies/strategy_runner.d.ts +5 -0
  351. package/types/src/core/strategies/transport_strategy.d.ts +15 -0
  352. package/types/src/core/strategies/websocket_prioritized_cached_strategy.d.ts +20 -0
  353. package/types/src/core/timeline/level.d.ts +6 -0
  354. package/types/src/core/timeline/timeline.d.ts +25 -0
  355. package/types/src/core/timeline/timeline_sender.d.ts +13 -0
  356. package/types/src/core/timeline/timeline_transport.d.ts +6 -0
  357. package/types/src/core/transports/assistant_to_the_transport_manager.d.ts +14 -0
  358. package/types/src/core/transports/ping_delay_options.d.ts +6 -0
  359. package/types/src/core/transports/transport.d.ts +8 -0
  360. package/types/src/core/transports/transport_connection.d.ts +35 -0
  361. package/types/src/core/transports/transport_connection_options.d.ts +6 -0
  362. package/types/src/core/transports/transport_hooks.d.ts +13 -0
  363. package/types/src/core/transports/transport_manager.d.ts +14 -0
  364. package/types/src/core/transports/transports_table.d.ts +10 -0
  365. package/types/src/core/transports/url_scheme.d.ts +11 -0
  366. package/types/src/core/transports/url_schemes.d.ts +4 -0
  367. package/types/src/core/user.d.ts +21 -0
  368. package/types/src/core/util.d.ts +8 -0
  369. package/types/src/core/utils/collections.d.ts +18 -0
  370. package/types/src/core/utils/factory.d.ts +29 -0
  371. package/types/src/core/utils/flat_promise.d.ts +6 -0
  372. package/types/src/core/utils/timers/abstract_timer.d.ts +10 -0
  373. package/types/src/core/utils/timers/index.d.ts +9 -0
  374. package/types/src/core/utils/timers/scheduling.d.ts +8 -0
  375. package/types/src/core/utils/timers/timed_callback.d.ts +4 -0
  376. package/types/src/core/utils/url_store.d.ts +4 -0
  377. package/types/src/core/watchlist.d.ts +8 -0
  378. package/types/src/runtimes/interface.d.ts +43 -0
  379. package/types/src/runtimes/isomorphic/auth/xhr_auth.d.ts +3 -0
  380. package/types/src/runtimes/isomorphic/default_strategy.d.ts +5 -0
  381. package/types/src/runtimes/isomorphic/http/http.d.ts +3 -0
  382. package/types/src/runtimes/isomorphic/http/http_xhr_request.d.ts +3 -0
  383. package/types/src/runtimes/isomorphic/runtime.d.ts +2 -0
  384. package/types/src/runtimes/isomorphic/timeline/xhr_timeline.d.ts +6 -0
  385. package/types/src/runtimes/isomorphic/transports/transport_connection_initializer.d.ts +1 -0
  386. package/types/src/runtimes/isomorphic/transports/transports.d.ts +5 -0
  387. package/types/src/runtimes/node/net_info.d.ts +6 -0
  388. package/types/src/runtimes/node/runtime.d.ts +3 -0
  389. package/types/src/runtimes/react-native/net_info.d.ts +8 -0
  390. package/types/src/runtimes/react-native/runtime.d.ts +3 -0
  391. package/types/src/runtimes/web/auth/jsonp_auth.d.ts +3 -0
  392. package/types/src/runtimes/web/browser.d.ts +19 -0
  393. package/types/src/runtimes/web/default_strategy.d.ts +5 -0
  394. package/types/src/runtimes/web/dom/dependencies.d.ts +4 -0
  395. package/types/src/runtimes/web/dom/dependency_loader.d.ts +10 -0
  396. package/types/src/runtimes/web/dom/jsonp_request.d.ts +10 -0
  397. package/types/src/runtimes/web/dom/script_receiver.d.ts +7 -0
  398. package/types/src/runtimes/web/dom/script_receiver_factory.d.ts +10 -0
  399. package/types/src/runtimes/web/dom/script_request.d.ts +9 -0
  400. package/types/src/runtimes/web/http/http.d.ts +2 -0
  401. package/types/src/runtimes/web/http/http_xdomain_request.d.ts +3 -0
  402. package/types/src/runtimes/web/net_info.d.ts +7 -0
  403. package/types/src/runtimes/web/runtime.d.ts +3 -0
  404. package/types/src/runtimes/web/timeline/jsonp_timeline.d.ts +6 -0
  405. package/types/src/runtimes/web/transports/transport_connection_initializer.d.ts +1 -0
  406. package/types/src/runtimes/web/transports/transports.d.ts +2 -0
  407. package/types/src/runtimes/worker/auth/fetch_auth.d.ts +3 -0
  408. package/types/src/runtimes/worker/net_info.d.ts +6 -0
  409. package/types/src/runtimes/worker/runtime.d.ts +3 -0
  410. package/types/src/runtimes/worker/timeline/fetch_timeline.d.ts +6 -0
  411. package/vite.config.js +52 -0
  412. package/vite.config.node.js +72 -0
  413. package/webpack/config.node.js +26 -0
  414. package/webpack/config.react-native.js +35 -0
  415. package/webpack/config.shared.js +50 -0
  416. package/webpack/config.web.js +36 -0
  417. package/webpack/config.worker.js +42 -0
  418. package/webpack/dev.server.js +17 -0
  419. package/webpack/hosting_config.js +6 -0
  420. package/with-encryption/index.d.ts +29 -0
  421. package/with-encryption/index.js +4 -0
  422. package/worker/index.d.ts +29 -0
  423. package/worker/index.js +1 -0
  424. package/worker/with-encryption/index.d.ts +29 -0
  425. package/worker/with-encryption/index.js +1 -0
@@ -0,0 +1,1363 @@
1
+ // app.js - Enhanced Pusher WebSocket Testing Dashboard
2
+
3
+ document.addEventListener('DOMContentLoaded', () => {
4
+ // DOM Elements
5
+ const elements = {
6
+ // Connection
7
+ configDisplay: document.getElementById('config-display'),
8
+ connectBtn: document.getElementById('connect-btn'),
9
+ disconnectBtn: document.getElementById('disconnect-btn'),
10
+ connectionStatus: document.getElementById('connection-status'),
11
+ statusDot: document.getElementById('status-dot'),
12
+
13
+ // Channels
14
+ channelNameInput: document.getElementById('channel-name'),
15
+ subscribeBtn: document.getElementById('subscribe-btn'),
16
+ subscribedChannels: document.getElementById('subscribed-channels'),
17
+ channelCount: document.getElementById('channel-count'),
18
+
19
+ // Server Events
20
+ serverEventChannel: document.getElementById('server-event-channel'),
21
+ serverEventName: document.getElementById('server-event-name'),
22
+ serverEventData: document.getElementById('server-event-data'),
23
+ sendServerEventBtn: document.getElementById('send-server-event-btn'),
24
+ sendBatchEventsBtn: document.getElementById('send-batch-events-btn'),
25
+
26
+ // Client Events
27
+ clientEventChannel: document.getElementById('client-event-channel'),
28
+ clientEventName: document.getElementById('client-event-name'),
29
+ clientEventData: document.getElementById('client-event-data'),
30
+ sendClientEventBtn: document.getElementById('send-client-event-btn'),
31
+
32
+ // Events Log
33
+ eventsLog: document.getElementById('events-log'),
34
+ clearEventsBtn: document.getElementById('clear-events-btn'),
35
+ exportEventsBtn: document.getElementById('export-events-btn'),
36
+
37
+ // Presence
38
+ presenceChannelName: document.getElementById('presence-channel-name'),
39
+ presenceCount: document.getElementById('presence-count'),
40
+ presenceMembers: document.getElementById('presence-members'),
41
+
42
+ // Statistics
43
+ totalEvents: document.getElementById('total-events'),
44
+ totalChannels: document.getElementById('total-channels'),
45
+ connectionTime: document.getElementById('connection-time'),
46
+ webhookCount: document.getElementById('webhook-count'),
47
+
48
+ // Webhooks
49
+ webhooksLog: document.getElementById('webhooks-log'),
50
+ fetchWebhooksBtn: document.getElementById('fetch-webhooks-btn'),
51
+ clearWebhooksBtn: document.getElementById('clear-webhooks-btn'),
52
+
53
+ // Delta Compression
54
+ deltaCompressionToggle: document.getElementById('delta-compression-toggle'),
55
+ deltaEnabled: document.getElementById('delta-enabled'),
56
+ deltaMessages: document.getElementById('delta-messages'),
57
+ fullMessages: document.getElementById('full-messages'),
58
+ bandwidthSaved: document.getElementById('bandwidth-saved'),
59
+ };
60
+
61
+ // Application State
62
+ let state = {
63
+ pusher: null,
64
+ config: null,
65
+ channels: new Map(),
66
+ currentPresenceChannel: null,
67
+ events: [],
68
+ webhooks: [],
69
+ stats: {
70
+ totalEvents: 0,
71
+ connectionStartTime: null,
72
+ connectionTimer: null,
73
+ },
74
+ currentEventFilter: 'all',
75
+ deltaCompression: {
76
+ enabled: false,
77
+ channelStates: new Map(), // Store last message per channel for delta decoding
78
+ stats: {
79
+ deltaMessages: 0,
80
+ fullMessages: 0,
81
+ totalBytesWithoutCompression: 0,
82
+ totalBytesWithCompression: 0,
83
+ },
84
+ },
85
+ };
86
+
87
+ // Utility Functions
88
+ const utils = {
89
+ formatTime(timestamp) {
90
+ return new Date(timestamp).toLocaleTimeString();
91
+ },
92
+
93
+ formatJSON(obj) {
94
+ try {
95
+ return JSON.stringify(obj, null, 2);
96
+ } catch (e) {
97
+ return String(obj);
98
+ }
99
+ },
100
+
101
+ getChannelType(channelName) {
102
+ if (channelName.startsWith('presence-')) return 'presence';
103
+ if (channelName.startsWith('private-')) return 'private';
104
+ return 'public';
105
+ },
106
+
107
+ exportEvents() {
108
+ const dataStr = JSON.stringify(state.events, null, 2);
109
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
110
+ const url = URL.createObjectURL(dataBlob);
111
+ const link = document.createElement('a');
112
+ link.href = url;
113
+ link.download = `pusher-events-${Date.now()}.json`;
114
+ link.click();
115
+ URL.revokeObjectURL(url);
116
+ },
117
+
118
+ updateConnectionTimer() {
119
+ if (state.stats.connectionStartTime) {
120
+ const elapsed = Date.now() - state.stats.connectionStartTime;
121
+ const minutes = Math.floor(elapsed / 60000);
122
+ const seconds = Math.floor((elapsed % 60000) / 1000);
123
+ elements.connectionTime.textContent = `${minutes
124
+ .toString()
125
+ .padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
126
+ }
127
+ },
128
+
129
+ addAnimation(element, animation = 'fade-in') {
130
+ element.classList.add(animation);
131
+ setTimeout(() => element.classList.remove(animation), 300);
132
+ },
133
+ };
134
+
135
+ // Event Management
136
+ const eventManager = {
137
+ add(event) {
138
+ event.id = Date.now() + Math.random();
139
+ state.events.unshift(event);
140
+ state.stats.totalEvents++;
141
+
142
+ // Keep only latest 500 events
143
+ if (state.events.length > 500) {
144
+ state.events = state.events.slice(0, 500);
145
+ }
146
+
147
+ eventManager.render();
148
+ eventManager.updateStats();
149
+ },
150
+
151
+ render() {
152
+ const filteredEvents = state.events.filter((event) => {
153
+ if (state.currentEventFilter === 'all') return true;
154
+ return event.type === state.currentEventFilter;
155
+ });
156
+
157
+ elements.eventsLog.innerHTML = '';
158
+
159
+ filteredEvents.forEach((event) => {
160
+ const li = document.createElement('li');
161
+ li.className = 'event-item';
162
+ li.innerHTML = `
163
+ <div class="event-header">
164
+ <div>
165
+ <span class="event-type ${event.type}">${event.type}</span>
166
+ <span class="event-title">${event.title}</span>
167
+ </div>
168
+ <span class="event-timestamp">${utils.formatTime(
169
+ event.timestamp,
170
+ )}</span>
171
+ </div>
172
+ ${
173
+ event.data
174
+ ? `<div class="event-data">${utils.formatJSON(event.data)}</div>`
175
+ : ''
176
+ }
177
+ `;
178
+ utils.addAnimation(li);
179
+ elements.eventsLog.appendChild(li);
180
+ });
181
+ },
182
+
183
+ updateStats() {
184
+ elements.totalEvents.textContent = state.stats.totalEvents;
185
+ elements.totalChannels.textContent = state.channels.size;
186
+ },
187
+
188
+ clear() {
189
+ state.events = [];
190
+ state.stats.totalEvents = 0;
191
+ eventManager.render();
192
+ eventManager.updateStats();
193
+ },
194
+ };
195
+
196
+ // Channel Management
197
+ const channelManager = {
198
+ subscribe(channelName) {
199
+ if (!state.pusher || state.pusher.connection.state !== 'connected') {
200
+ eventManager.add({
201
+ type: 'error',
202
+ title: 'Cannot subscribe: Not connected',
203
+ timestamp: Date.now(),
204
+ });
205
+ return;
206
+ }
207
+
208
+ if (state.channels.has(channelName)) {
209
+ eventManager.add({
210
+ type: 'system',
211
+ title: `Already subscribed to ${channelName}`,
212
+ timestamp: Date.now(),
213
+ });
214
+ return;
215
+ }
216
+
217
+ const channel = state.pusher.subscribe(channelName);
218
+ state.channels.set(channelName, channel);
219
+
220
+ channelManager.bindChannelEvents(channel, channelName);
221
+ channelManager.render();
222
+ channelManager.updateDropdowns();
223
+ },
224
+
225
+ unsubscribe(channelName) {
226
+ if (state.channels.has(channelName)) {
227
+ state.pusher.unsubscribe(channelName);
228
+ state.channels.delete(channelName);
229
+
230
+ if (
231
+ state.currentPresenceChannel &&
232
+ state.currentPresenceChannel.name === channelName
233
+ ) {
234
+ presenceManager.clear();
235
+ }
236
+
237
+ channelManager.render();
238
+ channelManager.updateDropdowns();
239
+ }
240
+ },
241
+
242
+ bindChannelEvents(channel, channelName) {
243
+ // Subscription events
244
+ channel.bind('pusher:subscription_succeeded', (data) => {
245
+ eventManager.add({
246
+ type: 'system',
247
+ title: `✅ Subscribed to ${channelName}`,
248
+ timestamp: Date.now(),
249
+ data: data,
250
+ });
251
+
252
+ if (channelName.startsWith('presence-')) {
253
+ state.currentPresenceChannel = channel;
254
+ presenceManager.update(channel.members);
255
+ }
256
+ });
257
+
258
+ channel.bind('pusher:subscription_error', (status) => {
259
+ eventManager.add({
260
+ type: 'error',
261
+ title: `❌ Subscription failed: ${channelName}`,
262
+ timestamp: Date.now(),
263
+ data: { status, channelName },
264
+ });
265
+ });
266
+
267
+ // Presence events
268
+ if (channelName.startsWith('presence-')) {
269
+ channel.bind('pusher:member_added', (member) => {
270
+ eventManager.add({
271
+ type: 'member',
272
+ title: `👋 Member joined ${channelName}`,
273
+ timestamp: Date.now(),
274
+ data: member,
275
+ });
276
+ if (state.currentPresenceChannel === channel) {
277
+ presenceManager.update(channel.members);
278
+ }
279
+ });
280
+
281
+ channel.bind('pusher:member_removed', (member) => {
282
+ eventManager.add({
283
+ type: 'member',
284
+ title: `👋 Member left ${channelName}`,
285
+ timestamp: Date.now(),
286
+ data: member,
287
+ });
288
+ if (state.currentPresenceChannel === channel) {
289
+ presenceManager.update(channel.members);
290
+ }
291
+ });
292
+ }
293
+
294
+ // Delta compression events
295
+ channel.bind('pusher:delta', (data) => {
296
+ console.log('[Delta] Received delta message, raw data:', data);
297
+
298
+ // Parse the data string if it's a string (Pusher protocol sends data as JSON string)
299
+ let deltaData;
300
+ if (typeof data === 'string') {
301
+ try {
302
+ deltaData = JSON.parse(data);
303
+ } catch (e) {
304
+ console.error('[Delta] Failed to parse delta data:', e);
305
+ return;
306
+ }
307
+ } else {
308
+ deltaData = data;
309
+ }
310
+
311
+ console.log('[Delta] Parsed delta data:', deltaData);
312
+
313
+ const decodedData = deltaCompressionManager.handleDeltaMessage(
314
+ channelName,
315
+ deltaData.event,
316
+ deltaData,
317
+ );
318
+
319
+ console.log('[Delta] Decoded data:', decodedData);
320
+
321
+ if (decodedData) {
322
+ const eventType = deltaData.event.startsWith('client-')
323
+ ? 'client'
324
+ : 'custom';
325
+ eventManager.add({
326
+ type: eventType,
327
+ title: `📡 ${deltaData.event} on ${channelName} (delta)`,
328
+ timestamp: Date.now(),
329
+ data: decodedData,
330
+ });
331
+ } else {
332
+ console.warn('[Delta] Decoded data was null or empty');
333
+ }
334
+ });
335
+
336
+ // Custom events (catch-all)
337
+ channel.bind_global((eventName, data) => {
338
+ if (!eventName.startsWith('pusher:')) {
339
+ console.log(
340
+ `[Delta] Event "${eventName}" on ${channelName}, data:`,
341
+ data,
342
+ );
343
+
344
+ // Note: Base message storage is handled by the WebSocket hook below
345
+ // which captures the raw message string before parsing.
346
+ // Don't reconstruct messages here as it can cause serialization mismatches!
347
+
348
+ const eventType = eventName.startsWith('client-')
349
+ ? 'client'
350
+ : 'custom';
351
+
352
+ eventManager.add({
353
+ type: eventType,
354
+ title: `📡 ${eventName} on ${channelName}`,
355
+ timestamp: Date.now(),
356
+ data: data,
357
+ });
358
+ }
359
+ });
360
+ },
361
+
362
+ render() {
363
+ elements.subscribedChannels.innerHTML = '';
364
+
365
+ state.channels.forEach((channel, channelName) => {
366
+ const div = document.createElement('div');
367
+ div.className = 'channel-item';
368
+
369
+ const channelType = utils.getChannelType(channelName);
370
+ div.innerHTML = `
371
+ <div>
372
+ <span class="channel-name">${channelName}</span>
373
+ <span class="channel-type ${channelType}">${channelType}</span>
374
+ </div>
375
+ <button class="btn btn-small btn-danger" onclick="channelManager.unsubscribe('${channelName}')">
376
+ <i class="fas fa-times"></i> Unsubscribe
377
+ </button>
378
+ `;
379
+
380
+ utils.addAnimation(div);
381
+ elements.subscribedChannels.appendChild(div);
382
+ });
383
+
384
+ elements.channelCount.textContent = state.channels.size;
385
+ },
386
+
387
+ updateDropdowns() {
388
+ [elements.serverEventChannel, elements.clientEventChannel].forEach(
389
+ (select) => {
390
+ const currentValue = select.value;
391
+ select.innerHTML = '<option value="">Select channel...</option>';
392
+
393
+ state.channels.forEach((channel, channelName) => {
394
+ const option = document.createElement('option');
395
+ option.value = channelName;
396
+ option.textContent = channelName;
397
+ if (channelName === currentValue) {
398
+ option.selected = true;
399
+ }
400
+ select.appendChild(option);
401
+ });
402
+ },
403
+ );
404
+
405
+ // Update client event button state
406
+ const hasSelectedChannel = elements.clientEventChannel.value !== '';
407
+ const isConnected = state.pusher?.connection?.state === 'connected';
408
+ elements.sendClientEventBtn.disabled =
409
+ !hasSelectedChannel || !isConnected;
410
+ },
411
+ };
412
+
413
+ // Presence Management
414
+ const presenceManager = {
415
+ update(members) {
416
+ if (!members) {
417
+ presenceManager.clear();
418
+ return;
419
+ }
420
+
421
+ elements.presenceChannelName.textContent =
422
+ state.currentPresenceChannel?.name || 'None';
423
+ elements.presenceCount.textContent = members.count || 0;
424
+
425
+ elements.presenceMembers.innerHTML = '';
426
+
427
+ if (members.count > 0) {
428
+ members.each((member) => {
429
+ const div = document.createElement('div');
430
+ div.className = 'member-item';
431
+ console.log('Member:', member);
432
+
433
+ const isMe = member.id === members.me?.id;
434
+ div.innerHTML = `
435
+ <img src="${
436
+ member.info.user_info?.avatar ||
437
+ `https://ui-avatars.com/api/?name=${encodeURIComponent(
438
+ member.info.user_info.name,
439
+ )}&background=random`
440
+ }" alt="${member.info.user_info.name}" class="member-avatar">
441
+ <div class="member-info">
442
+ <div class="member-name">${member.info.user_info.name}</div>
443
+ <div class="member-id">${member.info.user_id}</div>
444
+ </div>
445
+ ${isMe ? '<span class="member-badge">You</span>' : ''}
446
+ `;
447
+
448
+ utils.addAnimation(div);
449
+ elements.presenceMembers.appendChild(div);
450
+ });
451
+ } else {
452
+ elements.presenceMembers.innerHTML =
453
+ '<div class="member-item">No members present</div>';
454
+ }
455
+ },
456
+
457
+ clear() {
458
+ elements.presenceChannelName.textContent = 'None';
459
+ elements.presenceCount.textContent = '0';
460
+ elements.presenceMembers.innerHTML =
461
+ '<div class="member-item">Not subscribed to a presence channel</div>';
462
+ state.currentPresenceChannel = null;
463
+ },
464
+ };
465
+
466
+ // Connection Management
467
+ const connectionManager = {
468
+ async connect() {
469
+ if (!state.config) {
470
+ eventManager.add({
471
+ type: 'error',
472
+ title: 'Configuration not loaded',
473
+ timestamp: Date.now(),
474
+ });
475
+ return;
476
+ }
477
+
478
+ if (state.pusher && state.pusher.connection.state !== 'disconnected') {
479
+ eventManager.add({
480
+ type: 'system',
481
+ title: 'Already connected or connecting',
482
+ timestamp: Date.now(),
483
+ });
484
+ return;
485
+ }
486
+
487
+ connectionManager.updateStatus('connecting', 'Connecting...');
488
+
489
+ const pusherConfig = {
490
+ cluster: state.config.pusherCluster || 'mt1',
491
+ wsHost: state.config.pusherHost,
492
+ wsPort: state.config.pusherPort,
493
+ // wssPort: state.config.pusherPort,
494
+ forceTLS: state.config.pusherUseTLS,
495
+ enabledTransports: ['ws'],
496
+ disabledTransports: ['sockjs'],
497
+ authEndpoint: state.config.authEndpoint,
498
+ authTransport: 'ajax',
499
+ };
500
+
501
+ state.pusher = new Pusher(state.config.pusherKey, pusherConfig);
502
+ connectionManager.bindConnectionEvents();
503
+ },
504
+
505
+ disconnect() {
506
+ if (state.pusher) {
507
+ state.pusher.disconnect();
508
+ }
509
+ },
510
+
511
+ bindConnectionEvents() {
512
+ state.pusher.connection.bind('connected', () => {
513
+ state.stats.connectionStartTime = Date.now();
514
+ state.stats.connectionTimer = setInterval(
515
+ utils.updateConnectionTimer,
516
+ 1000,
517
+ );
518
+
519
+ connectionManager.updateStatus(
520
+ 'connected',
521
+ `Connected (${state.pusher.connection.socket_id})`,
522
+ );
523
+
524
+ eventManager.add({
525
+ type: 'system',
526
+ title: `🚀 Connected to WebSocket server`,
527
+ timestamp: Date.now(),
528
+ data: { socketId: state.pusher.connection.socket_id },
529
+ });
530
+
531
+ // Enable delta compression toggle now that we're connected
532
+ elements.deltaCompressionToggle.disabled = false;
533
+
534
+ elements.connectBtn.disabled = true;
535
+ elements.disconnectBtn.disabled = false;
536
+ elements.subscribeBtn.disabled = false;
537
+ channelManager.updateDropdowns();
538
+
539
+ // Install WebSocket hook now that connection is established
540
+ console.log('[Delta] Attempting to install WebSocket hook...');
541
+ connectionManager.installWebSocketHook();
542
+ console.log('[Delta] Hook installation attempt completed');
543
+ });
544
+
545
+ state.pusher.connection.bind('disconnected', () => {
546
+ if (state.stats.connectionTimer) {
547
+ clearInterval(state.stats.connectionTimer);
548
+ state.stats.connectionTimer = null;
549
+ }
550
+
551
+ connectionManager.updateStatus('disconnected', 'Disconnected');
552
+
553
+ eventManager.add({
554
+ type: 'system',
555
+ title: '🔌 Disconnected from server',
556
+ timestamp: Date.now(),
557
+ });
558
+
559
+ elements.connectBtn.disabled = false;
560
+ elements.disconnectBtn.disabled = true;
561
+ elements.subscribeBtn.disabled = true;
562
+
563
+ // Clear channels and presence
564
+ state.channels.clear();
565
+ channelManager.render();
566
+ channelManager.updateDropdowns();
567
+ presenceManager.clear();
568
+ });
569
+
570
+ state.pusher.connection.bind('connecting', () => {
571
+ connectionManager.updateStatus('connecting', 'Connecting...');
572
+ });
573
+
574
+ state.pusher.connection.bind('error', (err) => {
575
+ let errorMsg = 'Connection Error';
576
+ if (err.error?.data) {
577
+ errorMsg += `: ${err.error.data.code} - ${err.error.data.message}`;
578
+ } else if (err.message) {
579
+ errorMsg += `: ${err.message}`;
580
+ }
581
+
582
+ eventManager.add({
583
+ type: 'error',
584
+ title: errorMsg,
585
+ timestamp: Date.now(),
586
+ data: err,
587
+ });
588
+
589
+ connectionManager.updateStatus('error', 'Connection Error');
590
+ });
591
+
592
+ state.pusher.connection.bind('failed', () => {
593
+ eventManager.add({
594
+ type: 'error',
595
+ title: '❌ Connection failed permanently',
596
+ timestamp: Date.now(),
597
+ });
598
+
599
+ connectionManager.updateStatus('failed', 'Connection Failed');
600
+ elements.disconnectBtn.disabled = true;
601
+ });
602
+
603
+ // Delta compression events - bind globally to catch system-level events
604
+ state.pusher.bind_global((eventName, data, metadata) => {
605
+ console.log(`[Delta] Global event: ${eventName}, metadata:`, metadata);
606
+
607
+ if (eventName === 'pusher:delta_compression_enabled') {
608
+ state.deltaCompression.enabled = true;
609
+ deltaCompressionManager.updateUI();
610
+
611
+ eventManager.add({
612
+ type: 'system',
613
+ title: '✅ Delta compression enabled',
614
+ timestamp: Date.now(),
615
+ data: data,
616
+ });
617
+ }
618
+
619
+ // Store full messages for delta base (non-pusher events)
620
+ if (!eventName.startsWith('pusher:') && metadata && metadata.channel) {
621
+ // Reconstruct the full message as the server sent it
622
+ const fullMessage = {
623
+ event: eventName,
624
+ channel: metadata.channel,
625
+ data: data,
626
+ };
627
+ const messageStr = JSON.stringify(fullMessage);
628
+
629
+ const hasBase = state.deltaCompression.channelStates.has(
630
+ metadata.channel,
631
+ );
632
+ deltaCompressionManager.handleFullMessage(
633
+ metadata.channel,
634
+ messageStr,
635
+ );
636
+
637
+ if (!hasBase) {
638
+ console.log(
639
+ `[Delta] Stored base message for ${metadata.channel} (${messageStr.length} bytes)`,
640
+ );
641
+ }
642
+ }
643
+ });
644
+
645
+ state.pusher.connection.bind('pusher:error', (error) => {
646
+ if (error.data?.message?.includes('delta')) {
647
+ eventManager.add({
648
+ type: 'error',
649
+ title: `Delta compression error: ${error.data.message}`,
650
+ timestamp: Date.now(),
651
+ data: error,
652
+ });
653
+ }
654
+ });
655
+ },
656
+
657
+ installWebSocketHook(retryCount = 0) {
658
+ // Try multiple paths to find the WebSocket
659
+ const connection = state.pusher.connection.connection;
660
+ const transport = connection?.socket || connection?.transport?.socket;
661
+
662
+ console.log('[Delta] Hook installation attempt', {
663
+ retryCount,
664
+ hasConnection: !!connection,
665
+ hasSocket: !!connection?.socket,
666
+ hasTransport: !!connection?.transport,
667
+ hasTransportSocket: !!connection?.transport?.socket,
668
+ });
669
+
670
+ if (!transport) {
671
+ if (retryCount < 10) {
672
+ console.log(
673
+ `[Delta] WebSocket not ready, retry ${retryCount + 1}/10...`,
674
+ );
675
+ setTimeout(
676
+ () => connectionManager.installWebSocketHook(retryCount + 1),
677
+ 100,
678
+ );
679
+ } else {
680
+ console.error('[Delta] Failed to install hook after 10 retries');
681
+ }
682
+ return;
683
+ }
684
+
685
+ console.log('[Delta] Installing WebSocket message hook...');
686
+ const originalOnMessage = transport.onmessage;
687
+
688
+ transport.onmessage = function (event) {
689
+ try {
690
+ const message = JSON.parse(event.data);
691
+
692
+ // Log ALL messages for debugging
693
+ if (message.channel && message.event) {
694
+ console.log(
695
+ `[Delta] Raw WS message: event="${message.event}", channel="${message.channel}", data length=${event.data.length}`,
696
+ );
697
+ }
698
+
699
+ // Always store full messages as potential base for delta compression
700
+ // This captures the exact server format for delta base
701
+ // Skip pusher internal/system events (pusher: and pusher_internal:)
702
+ if (
703
+ message.channel &&
704
+ message.event &&
705
+ !message.event.startsWith('pusher:') &&
706
+ !message.event.startsWith('pusher_internal:')
707
+ ) {
708
+ // Check if this channel already has a base message
709
+ const hasBase = state.deltaCompression.channelStates.has(
710
+ message.channel,
711
+ );
712
+
713
+ // Store message regardless of whether compression is currently enabled
714
+ // This ensures we have a base when deltas start arriving
715
+ deltaCompressionManager.handleFullMessage(
716
+ message.channel,
717
+ event.data,
718
+ );
719
+
720
+ // Log for debugging
721
+ console.log(
722
+ `[Delta] ${hasBase ? 'UPDATED' : 'STORED'} base message for ${message.channel} (${event.data.length} bytes)`,
723
+ );
724
+ }
725
+ } catch (e) {
726
+ console.error('[Delta] Error in WebSocket hook:', e);
727
+ }
728
+
729
+ // Call original handler
730
+ if (originalOnMessage) {
731
+ originalOnMessage.call(this, event);
732
+ }
733
+ };
734
+
735
+ console.log('[Delta] ✓ WebSocket message hook installed successfully');
736
+ },
737
+
738
+ updateStatus(status, text) {
739
+ elements.connectionStatus.textContent = text;
740
+ elements.statusDot.className = `status-dot ${status}`;
741
+ },
742
+ };
743
+
744
+ // Server Events
745
+ const serverEventManager = {
746
+ async send() {
747
+ const channel = elements.serverEventChannel.value;
748
+ const eventName = elements.serverEventName.value.trim();
749
+ const eventDataStr = elements.serverEventData.value.trim();
750
+
751
+ if (!channel || !eventName) {
752
+ eventManager.add({
753
+ type: 'error',
754
+ title: 'Channel and event name are required',
755
+ timestamp: Date.now(),
756
+ });
757
+ return;
758
+ }
759
+
760
+ let eventData = {};
761
+ if (eventDataStr) {
762
+ try {
763
+ eventData = JSON.parse(eventDataStr);
764
+ } catch (e) {
765
+ eventManager.add({
766
+ type: 'error',
767
+ title: `Invalid JSON data: ${e.message}`,
768
+ timestamp: Date.now(),
769
+ });
770
+ return;
771
+ }
772
+ }
773
+
774
+ try {
775
+ const response = await fetch('/trigger-event', {
776
+ method: 'POST',
777
+ headers: { 'Content-Type': 'application/json' },
778
+ body: JSON.stringify({ channel, event: eventName, data: eventData }),
779
+ });
780
+
781
+ const result = await response.json();
782
+
783
+ if (result.success) {
784
+ eventManager.add({
785
+ type: 'system',
786
+ title: `📤 Server event sent: ${eventName} → ${channel}`,
787
+ timestamp: Date.now(),
788
+ data: eventData,
789
+ });
790
+
791
+ // Clear form
792
+ elements.serverEventName.value = '';
793
+ elements.serverEventData.value = '';
794
+ } else {
795
+ throw new Error(result.error || 'Failed to send event');
796
+ }
797
+ } catch (error) {
798
+ eventManager.add({
799
+ type: 'error',
800
+ title: `Failed to send server event: ${error.message}`,
801
+ timestamp: Date.now(),
802
+ });
803
+ }
804
+ },
805
+
806
+ async sendBatch() {
807
+ const channel = elements.serverEventChannel.value;
808
+
809
+ if (!channel) {
810
+ eventManager.add({
811
+ type: 'error',
812
+ title: 'Channel is required for batch events',
813
+ timestamp: Date.now(),
814
+ });
815
+ return;
816
+ }
817
+
818
+ try {
819
+ const response = await fetch('/trigger-batch-events', {
820
+ method: 'POST',
821
+ headers: { 'Content-Type': 'application/json' },
822
+ body: JSON.stringify({ channel, count: 5, delay: 500 }),
823
+ });
824
+
825
+ const result = await response.json();
826
+
827
+ if (result.success) {
828
+ eventManager.add({
829
+ type: 'system',
830
+ title: `📤 Batch events triggered on ${channel}`,
831
+ timestamp: Date.now(),
832
+ data: { message: result.message },
833
+ });
834
+ } else {
835
+ throw new Error(result.error || 'Failed to trigger batch events');
836
+ }
837
+ } catch (error) {
838
+ eventManager.add({
839
+ type: 'error',
840
+ title: `Failed to trigger batch events: ${error.message}`,
841
+ timestamp: Date.now(),
842
+ });
843
+ }
844
+ },
845
+ };
846
+
847
+ // Client Events
848
+ const clientEventManager = {
849
+ send() {
850
+ const channelName = elements.clientEventChannel.value;
851
+ const eventName = elements.clientEventName.value.trim();
852
+ const eventDataStr = elements.clientEventData.value.trim();
853
+
854
+ if (!channelName || !eventName) {
855
+ eventManager.add({
856
+ type: 'error',
857
+ title: 'Channel and event name are required',
858
+ timestamp: Date.now(),
859
+ });
860
+ return;
861
+ }
862
+
863
+ if (!eventName.startsWith('client-')) {
864
+ eventManager.add({
865
+ type: 'error',
866
+ title: 'Client event names must start with "client-"',
867
+ timestamp: Date.now(),
868
+ });
869
+ return;
870
+ }
871
+
872
+ let eventData = {};
873
+ if (eventDataStr) {
874
+ try {
875
+ eventData = JSON.parse(eventDataStr);
876
+ } catch (e) {
877
+ eventManager.add({
878
+ type: 'error',
879
+ title: `Invalid JSON data: ${e.message}`,
880
+ timestamp: Date.now(),
881
+ });
882
+ return;
883
+ }
884
+ }
885
+
886
+ const channel = state.channels.get(channelName);
887
+ if (!channel) {
888
+ eventManager.add({
889
+ type: 'error',
890
+ title: `Not subscribed to channel: ${channelName}`,
891
+ timestamp: Date.now(),
892
+ });
893
+ return;
894
+ }
895
+
896
+ try {
897
+ const triggered = channel.trigger(eventName, eventData);
898
+ if (triggered) {
899
+ eventManager.add({
900
+ type: 'client',
901
+ title: `📱 Client event sent: ${eventName} → ${channelName}`,
902
+ timestamp: Date.now(),
903
+ data: eventData,
904
+ });
905
+
906
+ // Clear form
907
+ elements.clientEventName.value = '';
908
+ elements.clientEventData.value = '';
909
+ } else {
910
+ throw new Error('Failed to trigger client event');
911
+ }
912
+ } catch (error) {
913
+ eventManager.add({
914
+ type: 'error',
915
+ title: `Failed to send client event: ${error.message}`,
916
+ timestamp: Date.now(),
917
+ });
918
+ }
919
+ },
920
+ };
921
+
922
+ // Webhook Management
923
+ const webhookManager = {
924
+ async fetch() {
925
+ try {
926
+ const response = await fetch('/webhooks-log');
927
+ const webhooks = await response.json();
928
+
929
+ elements.webhooksLog.innerHTML = '';
930
+ elements.webhookCount.textContent = webhooks.length;
931
+
932
+ if (webhooks.length === 0) {
933
+ elements.webhooksLog.innerHTML =
934
+ '<li class="webhook-item">No webhooks received yet</li>';
935
+ return;
936
+ }
937
+
938
+ webhooks.forEach((webhook) => {
939
+ const li = document.createElement('li');
940
+ li.className = 'webhook-item';
941
+
942
+ const events =
943
+ webhook.body?.events
944
+ ?.map((e) => `${e.name} (${e.channel || 'N/A'})`)
945
+ .join(', ') || 'No events';
946
+
947
+ li.innerHTML = `
948
+ <div class="event-header">
949
+ <div class="event-title">🪝 ${events}</div>
950
+ <div class="event-timestamp">${utils.formatTime(
951
+ webhook.timestamp,
952
+ )}</div>
953
+ </div>
954
+ <div class="event-data">${utils.formatJSON(webhook.body)}</div>
955
+ `;
956
+
957
+ utils.addAnimation(li);
958
+ elements.webhooksLog.appendChild(li);
959
+ });
960
+ } catch (error) {
961
+ eventManager.add({
962
+ type: 'error',
963
+ title: `Failed to fetch webhooks: ${error.message}`,
964
+ timestamp: Date.now(),
965
+ });
966
+ }
967
+ },
968
+
969
+ clear() {
970
+ elements.webhooksLog.innerHTML = '';
971
+ elements.webhookCount.textContent = '0';
972
+ },
973
+ };
974
+
975
+ // Configuration Loading
976
+ const loadConfig = async () => {
977
+ try {
978
+ const response = await fetch('/config');
979
+ if (!response.ok) {
980
+ throw new Error(`HTTP error! status: ${response.status}`);
981
+ }
982
+
983
+ state.config = await response.json();
984
+ elements.configDisplay.textContent = utils.formatJSON(state.config);
985
+ elements.connectBtn.disabled = false;
986
+
987
+ eventManager.add({
988
+ type: 'system',
989
+ title: '⚙️ Configuration loaded successfully',
990
+ timestamp: Date.now(),
991
+ });
992
+ } catch (error) {
993
+ elements.configDisplay.textContent = `Error loading config: ${error.message}`;
994
+ elements.connectBtn.disabled = true;
995
+
996
+ eventManager.add({
997
+ type: 'error',
998
+ title: `Configuration load failed: ${error.message}`,
999
+ timestamp: Date.now(),
1000
+ });
1001
+ }
1002
+ };
1003
+
1004
+ // Delta Compression Management
1005
+ const deltaCompressionManager = {
1006
+ // Strip delta metadata before caching bases so the stored bytes match the server's base
1007
+ sanitizeFullMessage(rawMessageString) {
1008
+ try {
1009
+ const metaKeys = [
1010
+ 'sequence',
1011
+ 'conflation_key',
1012
+ '__delta_seq',
1013
+ '__delta_full',
1014
+ '__delta_base_seq',
1015
+ '__conflation_key',
1016
+ ];
1017
+
1018
+ const stripMetadata = (obj) => {
1019
+ const cleaned = {};
1020
+ for (const key of Object.keys(obj)) {
1021
+ if (metaKeys.includes(key)) continue;
1022
+
1023
+ if (key === 'data' && obj[key]) {
1024
+ const dataVal = obj[key];
1025
+ if (typeof dataVal === 'string') {
1026
+ try {
1027
+ const parsedData = JSON.parse(dataVal);
1028
+ cleaned[key] = stripMetadata(parsedData);
1029
+ } catch {
1030
+ cleaned[key] = dataVal;
1031
+ }
1032
+ } else if (
1033
+ typeof dataVal === 'object' &&
1034
+ dataVal !== null &&
1035
+ !Array.isArray(dataVal)
1036
+ ) {
1037
+ cleaned[key] = stripMetadata(dataVal);
1038
+ } else {
1039
+ cleaned[key] = dataVal;
1040
+ }
1041
+ } else {
1042
+ cleaned[key] = obj[key];
1043
+ }
1044
+ }
1045
+ return cleaned;
1046
+ };
1047
+
1048
+ const parsed = JSON.parse(rawMessageString);
1049
+ const cleaned = stripMetadata(parsed);
1050
+ return JSON.stringify(cleaned);
1051
+ } catch (e) {
1052
+ // If anything fails, return the original payload
1053
+ return rawMessageString;
1054
+ }
1055
+ },
1056
+
1057
+ enable() {
1058
+ if (!state.pusher || state.pusher.connection.state !== 'connected') {
1059
+ return;
1060
+ }
1061
+
1062
+ state.pusher.connection.send_event('pusher:enable_delta_compression', {});
1063
+
1064
+ eventManager.add({
1065
+ type: 'system',
1066
+ title: '🗜️ Requesting delta compression...',
1067
+ timestamp: Date.now(),
1068
+ });
1069
+ },
1070
+
1071
+ disable() {
1072
+ state.deltaCompression.enabled = false;
1073
+ state.deltaCompression.channelStates.clear();
1074
+ deltaCompressionManager.updateUI();
1075
+
1076
+ eventManager.add({
1077
+ type: 'system',
1078
+ title: 'Delta compression disabled',
1079
+ timestamp: Date.now(),
1080
+ });
1081
+ },
1082
+
1083
+ handleDeltaMessage(channel, event, deltaData) {
1084
+ try {
1085
+ // Calculate the size of the delta message as it was received
1086
+ const deltaMessageWrapper = {
1087
+ event: 'pusher:delta',
1088
+ channel: channel,
1089
+ data: deltaData,
1090
+ };
1091
+ const compressedSize = JSON.stringify(deltaMessageWrapper).length;
1092
+
1093
+ // Get the last full message for this channel
1094
+ const lastMessage = state.deltaCompression.channelStates.get(channel);
1095
+
1096
+ if (!lastMessage) {
1097
+ console.warn(
1098
+ `No base message found for channel ${channel}, cannot decode delta`,
1099
+ );
1100
+ state.deltaCompression.stats.deltaMessages++;
1101
+ deltaCompressionManager.updateUI();
1102
+ return null;
1103
+ }
1104
+
1105
+ console.log(`[Delta] Base message for ${channel}:`, lastMessage);
1106
+ console.log(`[Delta] Base message length: ${lastMessage.length}`);
1107
+ console.log(`[Delta] Received delta (base64): ${deltaData.delta}`);
1108
+ console.log(`[Delta] Delta sequence: ${deltaData.seq}`);
1109
+ console.log(`[Delta] Algorithm: ${deltaData.algorithm || 'fossil'}`);
1110
+
1111
+ // Decode the delta using the appropriate algorithm
1112
+ const baseBytes = new TextEncoder().encode(lastMessage);
1113
+ const deltaBytes = Uint8Array.from(atob(deltaData.delta), (c) =>
1114
+ c.charCodeAt(0),
1115
+ );
1116
+ console.log(
1117
+ `[Delta] Base bytes length: ${baseBytes.length}, Delta bytes length: ${deltaBytes.length}`,
1118
+ );
1119
+
1120
+ let decodedBytes;
1121
+ const algorithm = deltaData.algorithm || 'fossil';
1122
+ try {
1123
+ if (algorithm === 'xdelta3') {
1124
+ console.log(`[Delta] Using xdelta3 (VCDIFF) decoder`);
1125
+ // Check if vcdiff decoder is available
1126
+ if (typeof vcdiff !== 'undefined') {
1127
+ // Use the global vcdiff object as documented
1128
+ // vcdiff.decode expects (delta, source) as Uint8Array
1129
+ decodedBytes = vcdiff.decode(deltaBytes, baseBytes);
1130
+ } else {
1131
+ throw new Error(
1132
+ 'VCDIFF decoder not found. Make sure https://cdn.ably.io/lib/vcdiff-decoder.min-1.js is loaded.',
1133
+ );
1134
+ }
1135
+ } else if (algorithm === 'fossil') {
1136
+ console.log(`[Delta] Using fossil decoder`);
1137
+ decodedBytes = fossilDelta.apply(baseBytes, deltaBytes);
1138
+ } else {
1139
+ throw new Error(`Unknown delta algorithm: ${algorithm}`);
1140
+ }
1141
+ console.log(
1142
+ `[Delta] Decoded bytes type:`,
1143
+ decodedBytes?.constructor?.name,
1144
+ `length:`,
1145
+ decodedBytes?.length,
1146
+ );
1147
+ } catch (error) {
1148
+ console.error(
1149
+ `[Delta] Failed to apply delta with ${algorithm}:`,
1150
+ error,
1151
+ );
1152
+ throw error;
1153
+ }
1154
+
1155
+ // Convert to Uint8Array if it's a plain Array
1156
+ const decodedUint8Array =
1157
+ decodedBytes instanceof Uint8Array
1158
+ ? decodedBytes
1159
+ : new Uint8Array(decodedBytes);
1160
+
1161
+ const decodedMessage = new TextDecoder().decode(decodedUint8Array);
1162
+ const sanitizedDecodedMessage =
1163
+ deltaCompressionManager.sanitizeFullMessage(decodedMessage);
1164
+
1165
+ // Update stats with actual sizes
1166
+ const fullSize = sanitizedDecodedMessage.length;
1167
+ state.deltaCompression.stats.totalBytesWithoutCompression += fullSize;
1168
+ state.deltaCompression.stats.totalBytesWithCompression +=
1169
+ compressedSize;
1170
+ state.deltaCompression.stats.deltaMessages++;
1171
+
1172
+ console.log(
1173
+ `[Delta] Decoded: ${fullSize} bytes (delta was ${compressedSize} bytes, saved ${fullSize - compressedSize} bytes, ${((1 - compressedSize / fullSize) * 100).toFixed(1)}%)`,
1174
+ );
1175
+
1176
+ // Store the decoded message as the new base for next delta
1177
+ console.log(
1178
+ `[Delta] Storing decoded message as new base (${sanitizedDecodedMessage.length} bytes)`,
1179
+ );
1180
+ console.log(
1181
+ `[Delta] New base content:`,
1182
+ sanitizedDecodedMessage.substring(0, 150),
1183
+ );
1184
+ state.deltaCompression.channelStates.set(
1185
+ channel,
1186
+ sanitizedDecodedMessage,
1187
+ );
1188
+ console.log(`[Delta] ✓ Base updated for channel ${channel}`);
1189
+
1190
+ deltaCompressionManager.updateUI();
1191
+
1192
+ // Parse and return the decoded data
1193
+ try {
1194
+ const fullMessage = JSON.parse(sanitizedDecodedMessage);
1195
+ // Return just the data field from the decoded message
1196
+ return fullMessage.data || fullMessage;
1197
+ } catch (e) {
1198
+ return sanitizedDecodedMessage;
1199
+ }
1200
+ } catch (error) {
1201
+ console.error('Failed to decode delta message:', error);
1202
+ eventManager.add({
1203
+ type: 'error',
1204
+ title: `Failed to decode delta message: ${error.message}`,
1205
+ timestamp: Date.now(),
1206
+ });
1207
+ return null;
1208
+ }
1209
+ },
1210
+
1211
+ handleFullMessage(channel, rawMessageString, sequence) {
1212
+ // Try to pull a sequence if not provided
1213
+ if (sequence === undefined) {
1214
+ try {
1215
+ const parsed = JSON.parse(rawMessageString);
1216
+ const dataObj =
1217
+ typeof parsed.data === 'string'
1218
+ ? JSON.parse(parsed.data)
1219
+ : parsed.data;
1220
+ sequence =
1221
+ parsed.sequence ??
1222
+ dataObj?.sequence ??
1223
+ parsed.__delta_seq ??
1224
+ dataObj?.__delta_seq ??
1225
+ 0;
1226
+ } catch {
1227
+ sequence = 0;
1228
+ }
1229
+ }
1230
+
1231
+ // Store the raw message string (after stripping delta metadata) exactly as server used for delta
1232
+ const sanitizedMessageString =
1233
+ deltaCompressionManager.sanitizeFullMessage(rawMessageString);
1234
+ const fullSize = sanitizedMessageString.length;
1235
+
1236
+ // Convert to hex for debugging
1237
+ const bytes = new TextEncoder().encode(sanitizedMessageString);
1238
+ const hexPreview = Array.from(bytes.slice(0, 100))
1239
+ .map((b) => b.toString(16).padStart(2, '0'))
1240
+ .join(' ');
1241
+
1242
+ console.log(
1243
+ `[Delta] Storing base message for ${channel} (${fullSize} bytes)`,
1244
+ );
1245
+ console.log(`[Delta] Base hex (first 100 bytes): ${hexPreview}`);
1246
+ console.log(
1247
+ `[Delta] Base text: ${sanitizedMessageString.substring(0, 200)}`,
1248
+ );
1249
+
1250
+ // Store as base message for future deltas
1251
+ state.deltaCompression.channelStates.set(channel, sanitizedMessageString);
1252
+
1253
+ // Update stats
1254
+ state.deltaCompression.stats.totalBytesWithoutCompression += fullSize;
1255
+ state.deltaCompression.stats.totalBytesWithCompression += fullSize;
1256
+ state.deltaCompression.stats.fullMessages++;
1257
+
1258
+ deltaCompressionManager.updateUI();
1259
+ },
1260
+
1261
+ updateUI() {
1262
+ elements.deltaEnabled.textContent = state.deltaCompression.enabled
1263
+ ? 'Yes'
1264
+ : 'No';
1265
+ elements.deltaMessages.textContent =
1266
+ state.deltaCompression.stats.deltaMessages;
1267
+ elements.fullMessages.textContent =
1268
+ state.deltaCompression.stats.fullMessages;
1269
+
1270
+ const totalUncompressed =
1271
+ state.deltaCompression.stats.totalBytesWithoutCompression;
1272
+ const totalCompressed =
1273
+ state.deltaCompression.stats.totalBytesWithCompression;
1274
+
1275
+ if (totalUncompressed > 0) {
1276
+ const savedBytes = totalUncompressed - totalCompressed;
1277
+ const savedPercent = ((savedBytes / totalUncompressed) * 100).toFixed(
1278
+ 1,
1279
+ );
1280
+ elements.bandwidthSaved.textContent = `${(savedBytes / 1024).toFixed(2)} KB (${savedPercent}%)`;
1281
+ } else {
1282
+ elements.bandwidthSaved.textContent = '0 KB (0%)';
1283
+ }
1284
+ },
1285
+ };
1286
+
1287
+ // Event Listeners
1288
+ elements.connectBtn.addEventListener('click', connectionManager.connect);
1289
+ elements.disconnectBtn.addEventListener(
1290
+ 'click',
1291
+ connectionManager.disconnect,
1292
+ );
1293
+
1294
+ elements.subscribeBtn.addEventListener('click', () => {
1295
+ const channelName = elements.channelNameInput.value.trim();
1296
+ if (channelName) {
1297
+ channelManager.subscribe(channelName);
1298
+ elements.channelNameInput.value = '';
1299
+ }
1300
+ });
1301
+
1302
+ elements.deltaCompressionToggle.addEventListener('change', (e) => {
1303
+ if (e.target.checked) {
1304
+ deltaCompressionManager.enable();
1305
+ } else {
1306
+ deltaCompressionManager.disable();
1307
+ }
1308
+ });
1309
+
1310
+ elements.channelNameInput.addEventListener('keypress', (e) => {
1311
+ if (e.key === 'Enter') {
1312
+ elements.subscribeBtn.click();
1313
+ }
1314
+ });
1315
+
1316
+ elements.sendServerEventBtn.addEventListener(
1317
+ 'click',
1318
+ serverEventManager.send,
1319
+ );
1320
+ elements.sendBatchEventsBtn.addEventListener(
1321
+ 'click',
1322
+ serverEventManager.sendBatch,
1323
+ );
1324
+
1325
+ elements.sendClientEventBtn.addEventListener(
1326
+ 'click',
1327
+ clientEventManager.send,
1328
+ );
1329
+ elements.clientEventChannel.addEventListener(
1330
+ 'change',
1331
+ channelManager.updateDropdowns,
1332
+ );
1333
+
1334
+ elements.clearEventsBtn.addEventListener('click', eventManager.clear);
1335
+ elements.exportEventsBtn.addEventListener('click', utils.exportEvents);
1336
+
1337
+ elements.fetchWebhooksBtn.addEventListener('click', webhookManager.fetch);
1338
+ elements.clearWebhooksBtn.addEventListener('click', webhookManager.clear);
1339
+
1340
+ // Event filter buttons
1341
+ document.querySelectorAll('.filter-btn').forEach((btn) => {
1342
+ btn.addEventListener('click', () => {
1343
+ document.querySelectorAll('.filter-btn').forEach((b) => {
1344
+ b.classList.remove('active');
1345
+ });
1346
+ btn.classList.add('active');
1347
+ state.currentEventFilter = btn.dataset.filter;
1348
+ eventManager.render();
1349
+ });
1350
+ });
1351
+
1352
+ // Make managers globally available for onclick handlers
1353
+ window.channelManager = channelManager;
1354
+
1355
+ // Initialize
1356
+ loadConfig();
1357
+ eventManager.render();
1358
+ presenceManager.clear();
1359
+ webhookManager.fetch();
1360
+
1361
+ // Auto-refresh webhooks every 30 seconds
1362
+ setInterval(webhookManager.fetch, 30000);
1363
+ });