feat: Drupal.rl thin JS API with request batching#45
Conversation
Introduces a shared Drupal.rl transport layer so multiple RL consumers on the same page produce ~2 requests instead of one per experiment: - js/rl.js exposes Drupal.rl.decide / turn / reward / flush. Decides flush on the next tick (catching every module that registers in Drupal.behaviors.attach); turns and rewards flush in a 500 ms window and via sendBeacon on visibilitychange / pagehide. - rl.php collapses the legacy turn/turns/reward/decide form handlers into a single action=batch JSON endpoint. ping is preserved for the hook_requirements() health check. - rl_page_attachments() publishes drupalSettings.rl.endpointUrl so consumer modules no longer have to compute and attach the URL themselves. - All four consumer modules (rl_example, rl_example_frontend, rl_menu_link, rl_page_title) migrated to the Drupal.rl API. The broken action=scores call in rl_example_frontend is replaced with Drupal.rl.decide(). - README and docs updated to describe the new JS API.
Drupal.rl is only one consumer of the batch endpoint. Native mobile apps, server-side workers, other CMSes, and edge functions can POST to rl.php directly using the same JSON protocol. Document the wire format, validation rules, error responses, and curl examples in both README.md and the project description HTML so those callers do not have to read rl.php to integrate.
|
I took a close pass through the new batching client,
I also ran |
Responds to the arm-ownership / ai_sorting feedback on #45. Deciding which variant to show belongs in PHP at render time where the consumer already owns the arm list (see ai_sorting's Views sort plugin and VariantSelectorBase in this module). Client-side decides would force runtime JS to know the current arm set, which drifts out of sync when experiment managers add or remove variants. - rl.php is now strictly additive: action=ping, action=turn, action=turns, and action=reward legacy form handlers are restored unchanged so ai_sorting and any other production consumer keeps working. The new action=batch sits alongside them as a JSON endpoint that Drupal.rl speaks. - action=batch carries only turns and rewards. The decides section is gone, and the response is now {"ok":true} instead of a decisions map. - js/rl.js drops Drupal.rl.decide() entirely and all of its promise / queue machinery. The API is now just turn(), reward(), and flush() plus pagehide sendBeacon. - rl_example_frontend is converted to the canonical pattern: the block decides the winning variant in PHP inside build() using ExperimentManager::getThompsonScores(), server-renders the winning button text, and exposes the arm id to Drupal.rl.turn() / .reward() via drupalSettings. Block picks up a 60-second cache max-age so the server-chosen variant can rotate as scores evolve. - README and HTML docs are rewritten around this split: "deciding" section for the PHP pattern, "JS API" section for tracking, "HTTP API" section documenting all five actions (ping / turn / turns / reward / batch) as peers.
- Add missing @param descriptions for $registry and $storage in handle_batch_request(). - Replace direct $_GET access with filter_input(INPUT_GET, ...) so the Drupal coding standards sniff for super globals stays happy. request_stack is not usable here because action dispatch has to happen before the Drupal kernel boots.
Absorbs PR #44 into the Drupal.rl batch transport so client-decide consumers (DXPR Builder and similar full-page-cache builders) share a single round trip with the turn/reward tracking already batched by Drupal.rl. - js/rl.js gains Drupal.rl.decide(experimentId, armIds) returning a Promise<armId>. Decides share the same 500 ms queue and the same POST as turns and rewards, so a page with a variant block plus other RL tracking ends up making one request instead of two. Fallback on server failure or missing decision resolves to armIds[0] so callers never need a .catch() for the common path. - rl.php handle_batch_request processes an optional decides section, calls ExperimentManager::getThompsonScores to seed cold-start priors, and returns a decisions map keyed by experiment id. The batch response becomes {"ok":true,"decisions":{...}}. - Documentation adds a "decide discipline" section: callers must read arm ids from a DOM attribute that the server-side renderer emitted, never hardcode. This mirrors ai_sorting's PHP pattern of recomputing arm ids from the current view query on every render and keeps JS drift-free without forcing the rl core to store arm lists. The convention (v0..vN, UUIDs, node ids) is whatever the builder emits; rl core is arm-agnostic.
Closes #42.
Summary
Introduces a shared
Drupal.rltransport layer so multiple RL consumers on the same page coalesce into a small, fixed number of requests instead of one pair per experiment. On a page running a hero CTA experiment plusrl_page_titleplus fourrl_menu_linkexperiments, network traffic drops from ~12 requests per page view to 2.js/rl.jsexposesDrupal.rl.decide/.turn/.reward/.flush.setTimeout 0), which catches every module registering synchronously duringDrupal.behaviors.attachin a single batch without any added latency.visibilitychangeandpagehideflush buffered events throughnavigator.sendBeaconso nothing is lost on navigation.Drupal.historyin Drupal core.rl.phpcollapses the legacyturn/turns/reward/decideform-encoded actions into a singleaction=batchJSON endpoint (payload shape documented in thehandle_batch_request()docblock).action=pingis preserved for thehook_requirements()health check.rl_page_attachments()publishesdrupalSettings.rl.endpointUrlso consumer modules no longer recompute and attach the URL themselves.rl_example_frontend- replaces the brokenaction=scorescall withDrupal.rl.decide().rl_example- replaces directsendBeaconwithDrupal.rl.turn().rl_menu_link- usesDrupal.rl.turn/.reward.rl_page_title- usesDrupal.rl.turn/.reward.*.libraries.ymlnow depends onrl/api.NewsletterBlockinrl_exampleandrl_example_frontenddrop the now-unusedmoduleExtensionListandrequestStackinjections since they no longer build the endpoint URL.rl_project_desc.html) updated to describe the new JS API and removed stalesendBeaconsnippets.Request reduction
sendBeaconPOST per event on navigationsendBeacononpagehideTest plan
rl_example_frontend,rl_page_title, and severalrl_menu_linkexperiments active.rl.php?action=batchat page load, with a JSON body containing all pending decides and initial turns.sendBeaconPOST torl.php?action=batchcarries any buffered events.drush rl:statusstill reflects the recorded turns and rewards after the above./admin/reports/statusshows therl.php accessibilityrequirement as OK (action=pingstill works).rl_example_frontendnewsletter button actually renders a variant (decide resolves), tracks a turn when the form enters the viewport, and tracks a reward on click.