240 lines
23 KiB
Markdown
240 lines
23 KiB
Markdown
# Async Bridge Spike Progress
|
|
|
|
## Goal
|
|
Determine whether frontend code can support `await context.jamClient.method()` for a browser/web-client implementation while preserving compatibility with existing synchronous native `window.jamClient` calls.
|
|
|
|
## Questions
|
|
- What is the current source of truth for native-vs-browser mode (`gon.isNativeClient`, `window.jamClient` presence, `jamClient.IsNativeClient()`, etc.)?
|
|
- Where is `window.jamClient` initialized/replaced (real native bridge vs fake/intercepted client)?
|
|
- Can `await` be introduced incrementally at callsites without breaking sync native behavior?
|
|
- Which core session-flow call chains would require async propagation first?
|
|
|
|
## Initial Plan
|
|
- [x] Identify existing native/browser detection logic and init path(s)
|
|
- [x] Find current jamClient interception/test hooks we can reuse for call-pattern recording
|
|
- [x] Select one session-related call chain as the async spike candidate
|
|
- [x] Document recommended compatibility pattern (native sync + web async via Promise-compatible calls)
|
|
|
|
## Notes
|
|
- Key assumption to validate: `await` on a synchronous native return value is safe (it should resolve immediately), allowing one code path if methods are called through `await` in async functions.
|
|
- Confirmed locally with Node runtime check: `await` on plain values/objects resolves immediately.
|
|
|
|
## Findings (2026-02-25)
|
|
- Native/browser mode detection already exists in multiple layers:
|
|
- `ClientHelper#is_native_client?` sets `gon.isNativeClient`, including the forced mode via cookie `act_as_native_client`.
|
|
- `homeScreen.js` toggles `act_as_native_client` with Ctrl+Shift+0 (`keyCode == 48`) when `gon.allow_force_native_client` is enabled.
|
|
- Runtime behavior also relies on `window.jamClient` presence and `context.jamClient.IsNativeClient()`.
|
|
- `JK.initJamClient(app)` in `web/app/assets/javascripts/utils.js` is the main installation point:
|
|
- if no `window.jamClient`, it installs `JK.FakeJamClient`.
|
|
- this is the natural replacement point for a future `JK.WebJamClient` in "pretend native/session-capable browser mode".
|
|
- Existing interception hook for recording exists in `JK.initJamClient(app)` (currently disabled under `else if(false)`), which wraps `window.jamClient` methods and logs call timing/returns.
|
|
- Async spike candidate call chain selected: session entry (`SessionStore` / `session.js` -> `sessionUtils.SessionPageEnter()` -> `context.jamClient.SessionPageEnter()`).
|
|
- This chain is a good spike because the return value is used immediately by `guardAgainstActiveProfileMissing`, so it exposes async propagation requirements early.
|
|
- Current blast-radius observation (session entry path):
|
|
- `SessionStore.js.coffee` and legacy `session.js` both call `sessionUtils.SessionPageEnter()` synchronously and immediately pass the result to `gearUtils.guardAgainstActiveProfileMissing(...)`.
|
|
- `guardAgainstActiveProfileMissing(...)` expects a plain object (`backendInfo.error`, `backendInfo.reason`), not a Promise.
|
|
- Therefore the first migration step cannot be "just make `SessionPageEnter()` async" without adapting both callers and guard handling.
|
|
- CoffeeScript asset pipeline does not currently support real `async/await` syntax in these files:
|
|
- local compile checks with `coffee-script` gem compile `await` as a normal identifier call (`await(y)`), producing invalid JS for native async/await semantics.
|
|
- implication: for CoffeeScript-heavy paths, prefer Promise/jQuery Deferred adapters first, not direct `await` syntax.
|
|
- Confirmed from `origin/promised_based_api_interation`: the team previously worked around this in `SessionStore.js.coffee` by embedding raw JavaScript via CoffeeScript backticks (e.g. ``onJoinSession: `async function(sessionId) { ... }` `` and `await context.jamClient...` inside).
|
|
- That branch also converted the session-entry path (`onJoinSessionDone`, `joinSession`, `waitForSessionPageEnterDone`) to async this way, which directly validates the spike target and migration shape.
|
|
- Confirmed local migration tool viability:
|
|
- `npx decaffeinate --version` -> `v8.1.4`
|
|
- Dry-run conversion of `SessionStore.js.coffee` produced structurally usable output (method/object style preserved closely enough for follow-on edits).
|
|
- Decaffeinate outputs modern JS syntax, so the correct target in this repo is `.es6` (to stay on the existing `sprockets-es6` pipeline) rather than plain `.js`.
|
|
|
|
## Changes Applied (2026-02-25)
|
|
- Migrated `web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee` to `web/app/assets/javascripts/react-components/stores/SessionStore.es6` using `decaffeinate`.
|
|
- Removed the CoffeeScript source file for `SessionStore`.
|
|
- Basic syntax parse check passed by copying the `.es6` file to a temporary `.js` filename and running `node --check` (Node does not natively parse `.es6` extension files directly).
|
|
- Added parser-safe dual bundle delivery scaffolding for the `/client` layout:
|
|
- `web/app/assets/javascripts/client_legacy.js`
|
|
- `web/app/assets/javascripts/client_modern.js`
|
|
- `web/app/views/layouts/client.html.erb` now selects bundle server-side via `legacy_qt_webkit_client?`
|
|
- `web/app/helpers/client_helper.rb#legacy_qt_webkit_client?` uses UA-only detection (ignores forced-native cookie)
|
|
- Added asset precompile entries for both bundles in `web/config/application.rb` and `web/config/environments/test.rb`
|
|
- Split `SessionStore` loading by bundle:
|
|
- restored legacy `web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee` (legacy/default path)
|
|
- moved modern JS-converted store to `web/app/assets/javascripts/react-components/stores/SessionStoreModern.es6`
|
|
- added `web/app/assets/javascripts/react-components_modern.js` to load `SessionStoreModern` explicitly and avoid `require_directory ./react-components/stores` pulling the legacy store
|
|
- added `web/app/assets/javascripts/application_client_modern.js` (copy of `application.js` with `react-components_modern`)
|
|
- `web/app/assets/javascripts/client_modern.js` now requires `application_client_modern`
|
|
- Added `//= require babel/polyfill` to `web/app/assets/javascripts/application_client_modern.js` so Babel 5 async/await transpilation has `regeneratorRuntime` available in the modern client bundle.
|
|
- Converted the modern-only session-entry path in `web/app/assets/javascripts/react-components/stores/SessionStoreModern.es6` to native `async/await`:
|
|
- `onJoinSession`
|
|
- `onJoinSessionDone` (new helper extracted from the nested deferred chain)
|
|
- `await` now wraps mixed sync/thenable results (`rest`, jQuery Deferreds, and sync `SessionPageEnter()` values) in the modern path only.
|
|
- Continued the modern-only async conversion in `SessionStoreModern.es6`:
|
|
- `waitForSessionPageEnterDone` -> `async`
|
|
- `joinSession` -> `async`
|
|
- Preserved `this.joinDeferred` as the original jQuery Deferred/XHR object so existing `.state()` checks and `.done()` listeners still work.
|
|
- Fixed `audio_latency` retrieval to await `FTUEGetExpectedLatency()` before reading `.latency`.
|
|
- Added mode-aware browser jamClient installation seam in `web/app/assets/javascripts/utils.js`:
|
|
- if no native `window.jamClient` and `gon.isNativeClient && JK.WebJamClient` exists, install `WebJamClient`
|
|
- otherwise fall back to `FakeJamClient` (current behavior)
|
|
- Added initial `web/app/assets/javascripts/webJamClient.js` shim:
|
|
- delegates to `FakeJamClient` (method/property passthrough) so the seam is exercised without changing behavior yet
|
|
- exposes `IsWebClient()` marker and retains conservative `IsNativeClient() -> false` for now
|
|
- keeps `__delegate` handle to support incremental method replacement/testing
|
|
- Loaded `webJamClient.js` from `web/app/assets/javascripts/everywhere/everywhere.js` before `JK.initJamClient(...)` runs.
|
|
- Expanded `WebJamClient` with first real overrides and internal state bookkeeping:
|
|
- callback registrations (`SessionRegisterCallback`, `RegisterRecordingCallbacks`, `RegisterSessionJoinLeaveRequestCallBack`)
|
|
- session lifecycle (`SessionPageEnter`, `SessionPageLeave`, `JoinSession`, `LeaveSession`)
|
|
- connection refresh rate tracking (`SessionSetConnectionStatusRefreshRate`)
|
|
- `GetWebClientDebugState()` for inspection during manual testing
|
|
- Added minimal local-media bootstrap placeholders in `WebJamClient` (disabled by default):
|
|
- optional `getUserMedia({audio:true})` auto-start behind flags only
|
|
- tracks local media status in debug state; stops tracks on page/session leave transitions
|
|
- **Instrumentation hooks implemented (disabled by default, ready for manual recording sessions):**
|
|
- records jamClient calls/returns/throws + thenable resolve/reject events + session state transitions inside `WebJamClient`
|
|
- clearly gated by:
|
|
- `window.JK_WEB_CLIENT_RECORDING_ENABLED = true`, or
|
|
- `localStorage['jk.webClient.recording'] = '1'`, or
|
|
- `gon.web_client_recording_enabled`
|
|
- recorder control methods:
|
|
- `jamClient.WebClientRecorderEnable()`
|
|
- `jamClient.WebClientRecorderDisable()`
|
|
- `jamClient.WebClientRecorderClear()`
|
|
- `jamClient.WebClientRecorderGetLog()`
|
|
- recorder is intended as TEMP/manual capture support and is easy to disable/remove later.
|
|
- Implemented native QtWebKit bridge call instrumentation in `web/app/assets/javascripts/utils.js` via adapter wrapping (disabled by default):
|
|
- installs `window.jamClientAdapter` (does **not** swap/replace the special QtWebKit `window.jamClient` object)
|
|
- native adapter wraps and forwards to the real Qt bridge object during `JK.initJamClient(...)`
|
|
- exposes recorder controls on `JK` for manual native capture sessions
|
|
- recorder controls on `JK`:
|
|
- `JK.enableNativeJamClientRecording()`
|
|
- `JK.disableNativeJamClientRecording()`
|
|
- `JK.clearNativeJamClientRecording()`
|
|
- `JK.getNativeJamClientRecording()`
|
|
- enable auto-recording with:
|
|
- `window.JK_NATIVE_CLIENT_RECORDING_ENABLED = true`, or
|
|
- `localStorage['jk.nativeJamClient.recording'] = '1'`, or
|
|
- `gon.native_client_recording_enabled`
|
|
- caveat: wrapping depends on `Object.keys(window.jamClient)` exposing Qt bridge method keys (the app already had a dormant interceptor using this same mechanism, which is why this approach is viable).
|
|
- Pivoted callsite strategy (per native client constraint): migrated frontend callsites to use `jamClientAdapter` instead of direct `jamClient` access so instrumentation does not require replacing the Qt bridge object.
|
|
- Bulk-updated app frontend assets and inline views; remaining direct `jamClient` references are intentionally limited to `utils.js` bootstrap/adapter internals and object creation paths.
|
|
|
|
## Recommended Direction (Initial)
|
|
- Treat `gon.isNativeClient` as "session-capable client mode" (includes real native + forced browser emulation).
|
|
- Distinguish actual bridge availability by checking native `window.jamClient` presence and/or `jamClient.IsNativeClient()`.
|
|
- Replace `FakeJamClient` installation in `JK.initJamClient(app)` with mode-aware behavior:
|
|
- real native bridge present: use native `window.jamClient`
|
|
- `gon.isNativeClient == true` but no native bridge: install `JK.WebJamClient`
|
|
- normal browser pages (`gon.isNativeClient == false`): retain minimal fake/stub behavior as needed (or a reduced shim)
|
|
- Introduce `await` incrementally on selected jamClient call chains; native sync returns remain compatible under `await`, but caller functions must become `async` (or return Promises / deferreds) as propagation proceeds.
|
|
- Split migration strategy by file/runtime:
|
|
- ES6 modules/wrappers can use `async/await` directly.
|
|
- CoffeeScript/jQuery chains should use a Promise/Deferred normalization helper (e.g., `JK.whenJamClientResult(result)` / `Promise.resolve(result)` + bridge to `$.Deferred`) until those paths are moved out of CoffeeScript.
|
|
- First practical spike implementation should add a parallel method instead of changing existing sync method in place:
|
|
- keep `sessionUtils.SessionPageEnter()` (sync contract)
|
|
- add `sessionUtils.SessionPageEnterDeferred()` (or similar) that normalizes sync/async jamClient returns into a `$.Deferred`
|
|
- update one caller path (prefer `SessionStore`) behind a feature flag or mode check for `WebJamClient`
|
|
- Alternative (proven but ugly): use backtick-embedded raw JS `async function` blocks in `SessionStore.js.coffee` for the spike.
|
|
- Preferred longer-term direction for maintainability: migrate high-churn CoffeeScript session/store files to JS/ES modules instead of expanding the backtick pattern.
|
|
- `SessionStore` migration is now done, so the next spike step should use native `async` in `SessionStore.es6` for the session-entry path.
|
|
- Raw `async/await` remains unsafe for legacy QtWebKit if delivered directly; the new dual bundle switch is the mechanism to keep parser-incompatible syntax out of the legacy client.
|
|
- Next validation needed before introducing `async` in `SessionStoreModern.es6`: confirm the `sprockets-es6` pipeline used here can parse/emit files containing `async/await` syntax (or at least pass through for modern browsers).
|
|
- Validation results:
|
|
- `babel-transpiler` (Babel 5.8 via `sprockets-es6`) successfully transpiles async/await and emits `regeneratorRuntime`-based code.
|
|
- `SessionStoreModern.es6` passes syntax parse via `node --check` (on a temporary `.js` copy).
|
|
- After additional edits, `SessionStoreModern.es6` still passes `node --check` and `Babel::Transpiler.transform(...)` (`BABEL_OK`).
|
|
- Rails/Sprockets asset lookup validation (serial run) succeeded for both client bundles:
|
|
- `MODERN_FOUND`
|
|
- `LEGACY_FOUND`
|
|
- `MODERN_HAS_REGEN`
|
|
- `MODERN_HAS_SESSIONSTOREMODERN_CODE`
|
|
- `MODERN_NO_BAD_UNDEFINED_ACTIONS`
|
|
- `LEGACY_NO_SESSIONSTOREMODERN_REF`
|
|
- `LEGACY_NO_BAD_UNDEFINED_ACTIONS`
|
|
- Practical note: Rails boot in this repo starts websocket/EventMachine side effects, so validation commands should be run serially (parallel boots can conflict on port `6767` and produce false-negative noise).
|
|
- Local JS seam simulation confirmed `JK.initJamClient(...)` selects `WebJamClient` when `gon.isNativeClient == true`, no native `window.jamClient` is present, and `JK.WebJamClient` is defined.
|
|
- Local JS runtime simulation validated `WebJamClient` constructor, recorder controls, session state updates, and debug-state reporting.
|
|
- `utils.js` native adapter instrumentation patch passes syntax parse (`node --check` on a temp copy).
|
|
- Full Rails asset compile/runtime validation is still pending; direct `rails runner` asset probing booted websocket/EventMachine side effects in this repo and was not a clean validation path.
|
|
- Follow-up debugging from legacy client load (console log capture):
|
|
- Legacy QtWebKit was still receiving modern manifest code because `application.js` has `//= require_directory .`, and the newly-added top-level manifests (`client_modern`, `application_client_modern`, `react-components_modern`) were accidentally included in the legacy/default bundle.
|
|
- Fixed by moving all new manifests under `web/app/assets/javascripts/client_bundles/` and updating layout/precompile entries to use `client_bundles/client_legacy` and `client_bundles/client_modern`.
|
|
- Also fixed decaffeinate strict-mode top-level references in `SessionStoreModern.es6` (`this` -> `window/context` for action bindings and `SessionStore` export) so Babel `"use strict"` does not produce `undefined.JamTrackActions`.
|
|
- Legacy parser/runtime follow-ups from hosted `console.log`:
|
|
- `fb-error` / `Expected token 'in'` traced to Facebook SDK script (`connect.facebook.net/.../all.js`) using `for...of` (unsupported by old QtWebKit parser).
|
|
- Initial selective skip in `everywhere.js` caused UI startup regressions because downstream code expected `facebookHelper.deferredLoginStatus()` to always return a Deferred.
|
|
- Stabilized by moving disable behavior into `facebook_helper.js`:
|
|
- FB SDK is now hard-disabled via `shouldEnableSdk() -> false` (temporary), while still creating/resolving `loginStatusDeferred` with `{status: 'disabled'}` so call sites remain functional.
|
|
- `promptLogin()` now resolves immediately to disabled response when SDK disabled.
|
|
- `deferredLoginStatus()` now guarantees a non-null resolved Deferred even before `initialize()`.
|
|
- Reverted special init branching in `everywhere.js`; initialization flow is again normal (`facebookHelper.initialize(...)`) but helper itself no-ops SDK load safely.
|
|
- Investigated session join 409 (`PG::UniqueViolation`) and found duplicate `participant_create` requests with identical payload/session id.
|
|
- Likely cause in modern bundle: both `SessionStoreModern` and legacy `SessionStore` can be loaded (via `application_client_modern.js` `require_directory ..` pulling top-level `react-components.js`), leading to duplicate Reflux `SessionActions.joinSession` handlers.
|
|
- Mitigation added:
|
|
- `SessionStoreModern.es6` sets marker `window.__JK_USE_MODERN_SESSION_STORE__ = true` before creating store.
|
|
- `SessionStore.js.coffee` now aliases to existing store when marker is set (`@SessionStore = if context.__JK_USE_MODERN_SESSION_STORE__ then context.SessionStore else Reflux.createStore(...)`) so legacy store does not register duplicate listeners in modern bundle.
|
|
- Kept `require_directory ..` in modern application manifest to avoid regressing other root-level script dependencies.
|
|
- Added REST-level single-flight guard in `JK.Rest.joinSession` (`web/app/assets/javascripts/jam_rest.js`) keyed by `session_id|client_id`.
|
|
- Duplicate in-flight join calls now reuse the same jqXHR instead of issuing a second POST.
|
|
- Added cleanup on `always()` to release key after completion.
|
|
- Hardened implementation to avoid mutating caller `options` object (payload cloned before removing `session_id`).
|
|
- Added high-signal debug instrumentation for duplicate join investigation:
|
|
- `jam_rest.js`: join dedupe map moved from per-instance to global shared map (`JK.__pendingJoinSessionRequests`) because multiple `JK.Rest()` instances exist.
|
|
- `jam_rest.js`: logs `[join-dedupe] create|reuse|release` with `dedupe_key`, `rest_instance_id`, and `trace_token`.
|
|
- `SessionStore.js.coffee`: logs legacy store load/init/onJoinSession (`[session-store] legacy-*`) with marker/count context.
|
|
- `SessionStoreModern.es6`: logs modern store load/init/onJoinSession (`[session-store] modern-*`) with load-count context.
|
|
- Added extra join-source tracing because duplicate attempt source is still unknown:
|
|
- `session_utils.js`: logs `[join-source] SessionUtils.joinSession` with call count + stack.
|
|
- `CallbackStore.js.coffee`: logs `[join-source] generic-callback join_session` with payload + stack before dispatching `SessionActions.joinSession`.
|
|
- This should distinguish UI-initiated join from native/generic-callback initiated join.
|
|
- Root-cause analysis from console logs: duplicate join came from two SessionStore pipelines running (legacy + modern), not from REST payload issues.
|
|
- Why prior marker was insufficient: load-order race when `react-components.js` is included indirectly (`require_directory ..`), where legacy SessionStore may load before modern marker is set.
|
|
- Added load-order-independent modern mode flag:
|
|
- `client_bundles/session_store_modern_mode.js` sets `window.__JK_SKIP_LEGACY_SESSION_STORE__ = true`.
|
|
- `application_client_modern.js` now requires this flag before broad includes.
|
|
- `SessionStore.js.coffee` now no-ops when either `__JK_SKIP_LEGACY_SESSION_STORE__` or `__JK_USE_MODERN_SESSION_STORE__` is set.
|
|
- Implemented `debugging_console_spec.md` diversion:
|
|
- Added env-gated gon flag: `gon.log_to_server` set from `ENV['LOG_TO_SERVER'] == '1'` in `client_helper.rb`.
|
|
- Added new API endpoint `POST /api/debug_console_logs` (`ApiDebugConsoleLogsController#create`) that writes pretty JSON log dumps to `web/tmp/console-logs/YYYYMMDD-HHMMSS-<label>.log`.
|
|
- Label sanitization implemented per spec: trim + remove all whitespace/newlines; defaults to `log` when blank.
|
|
- Added client-side in-memory collector `debug_log_collector.js` (enabled only when `gon.log_to_server`):
|
|
- captures REST activity by wrapping `jQuery.ajax` (`rest.request`, `rest.response`, `rest.error`).
|
|
- exposes `JK.DebugLogCollector.push/getBuffer/clear/promptAndUpload`.
|
|
- prompts user on `EVENTS.SESSION_ENDED` for label via `prompt(...)`, uploads to new API, and alerts result.
|
|
- Integrated jam-bridge and websocket capture into collector:
|
|
- `utils.js` native jamClient adapter `pushRecord(...)` now forwards events into collector.
|
|
- `JamServer.js` logs websocket send/receive payloads into collector.
|
|
- Added manifests so collector loads in both bundles:
|
|
- `application.js`
|
|
- `client_bundles/application_client_modern.js`
|
|
- Reduced debug UI noise per request:
|
|
- Removed post-upload `window.alert` popups from `debug_log_collector.js` (prompt remains; outcomes now recorded in buffer as `debug-log-collector.uploaded` / `debug-log-collector.upload-error`).
|
|
- Removed temporary native jamClient console mirroring in `utils.js`; native bridge instrumentation remains buffer-based.
|
|
- Removed `join-dedupe` console/logger output from `jam_rest.js`; join dedupe telemetry now goes to debug buffer only (`join-dedupe.create|reuse|release`).
|
|
- Fixed debug log file double-encoding in `ApiDebugConsoleLogsController`.
|
|
- `logs` payload is now normalized recursively (`ActionController::Parameters`/arrays/hashes) into plain JSON-friendly structures before writing.
|
|
- Added safe best-effort JSON string parsing for string values that are valid JSON.
|
|
- Added REST request/response correlation IDs in debug collector.
|
|
- `rest.request`, `rest.response`, and `rest.error` now include `request_id` (`rest-<seq>`), and response/error include `duration_ms`.
|
|
- Continued root-cause instrumentation for duplicate join attempts, with all new signals routed into `DebugLogCollector` buffer:
|
|
- `SessionActions.joinSession` wrapper now captures both direct call and `.trigger(...)` invocations (`join-source.session-action`) with stack.
|
|
- `session.js` (legacy SessionScreen path) now logs:
|
|
- `join-source.session-screen.afterShow`
|
|
- `join-source.session-screen.afterCurrentUserLoaded`
|
|
- `join-source.session-screen.sessionModel.joinSession`
|
|
- `sessionModel.js` now logs:
|
|
- `join-source.session-model.joinSession`
|
|
- `join-source.session-model.joinSessionRest`
|
|
- `SessionStore.js.coffee` now logs to buffer:
|
|
- `join-source.session-store.legacy-load`
|
|
- `join-source.session-store.legacy-init`
|
|
- `join-source.session-store.legacy-onJoinSession`
|
|
- `SessionStoreModern.es6` now logs to buffer:
|
|
- `join-source.session-store.modern-load`
|
|
- `join-source.session-store.modern-init`
|
|
- `CallbackStore.js.coffee` now records generic callback joins in buffer (`join-source.generic-callback.join_session`) and removed console spam.
|
|
- `session_utils.js` join-source trace now writes to buffer (`join-source.session-utils.joinSession`) while downgrading console severity.
|
|
- Added a hard gate in `SessionStoreModern.es6` so the modern Reflux store only installs when `window.__JK_SKIP_LEGACY_SESSION_STORE__` is true.
|
|
- In legacy bundle loads, modern store now emits `join-source.session-store.modern-skipped` and does not register listeners; this prevents hidden second `onJoinSession` listeners from racing the legacy store.
|
|
- Added targeted leave-source instrumentation to diagnose immediate post-join session departure:
|
|
- `SessionActions.leaveSession` now emits `leave-source.session-action` (call + trigger wrappers, with stack) in `react-components/actions/SessionActions.js.coffee`.
|
|
- `SessionStore.js.coffee#onLeaveSession` now emits `leave-source.session-store.legacy-onLeaveSession`.
|
|
- `SessionStoreModern.es6#onLeaveSession` now emits `leave-source.session-store.modern-onLeaveSession`.
|
|
- This is intended to identify the exact caller path behind immediate `DELETE /api/participants/:client_id` after successful join.
|