Cursor cache mechanism

Why it exists

The phone-data response is large — 50–200 KB common, larger for sites with many forms or content classes. Re-sending the entire response on every cold app launch would be wasteful (mobile data, app start latency) and pointless (most sections rarely change).

The cursor mechanism lets the app fetch only the sections that have changed since its last successful request.


How it works

1. Each section of the response is timestamp-tagged when built
   (form definitions, button arrays, content sections, notification list).

2. The mobile app stores a `cursor` value — the last "as of" timestamp it has.

3. On the next request, mobile sends `?cursor=<ts>`.

4. The pipeline compares the cursor against per-section timestamps:
     - sections newer than cursor → sent in full
     - sections older than cursor → sent as `null`
       (the app keeps its cached copy)

5. When a setting change invalidates the entire cache,
   `JSON_Cursor_Manager::reset_cursor()` bumps the per-user cursor.

JSON_Cursor_Manager

includes/app-connect/json-cursor-manager.php owns the cursor state. Key public methods:

Method Use
reset_cursor() Invalidate the entire per-user cache (forces full rebuild on next request)
get_cursor( $user_id ) Read the user’s current cursor value
set_cursor( $user_id, $value ) Write a new cursor (called by the pipeline at end-of-build)
should_skip_section( $section_key, $cursor_value ) Per-section comparison — returns true to short-circuit and serve null

The cursor is per-user — each user’s app holds its own cursor.


When reset_cursor() is called

  • Admin saves a button (mam_app_settings_set_button)
  • Admin uploads a new image (local_app_update_image)
  • Admin saves a layout (mam_app_settings_set_layout_settings)
  • Push credentials change
  • Theme colors change
  • Any setting whose change should cascade to every user

⚠️ Calling reset_cursor() on every update_option would defeat the cache globally. Use it only for setting changes that actually require a full mobile rebuild.


bypass_caching flag

Set in phase_settings when:

  • The user is in cloning mode (MAM_Current_Request->is_cloning()) — admins previewing as another role need fresh data on every request
  • The user has caching disabled in their settings (tsl-setting-disable_caching)

When bypass_caching is on, the cursor is ignored and every section is rebuilt. Useful for admin previews; expensive in production.


Per-section timestamps

Sections are timestamp-tagged at build time. The pipeline keeps a section-key → timestamp map so it can answer “is section X newer than cursor C?” in constant time.

Section keys include:

  • forms_data — Gravity Forms cache version
  • button_array_<role> — per-role button definitions
  • home_screen_stack — layout
  • notification_history_<user> — per-user notifications
  • Whatever sibling plugins register

Subscriber pattern for cache-aware sections

Sibling plugins can short-circuit their own contributions when the cursor indicates no changes:

add_filter( 'mam_get_phone_data_before_send', function ( $data_array ) {
    $cursor = JSON_Cursor_Manager::get_cursor( get_current_user_id() );
    $my_section_ts = get_option( 'my_plugin_data_changed_at', 0 );

    if ( $my_section_ts <= $cursor ) {
        $data_array['my_section'] = null;  // signal "no change"
        return $data_array;
    }

    $data_array['my_section'] = $this->build_my_section();
    return $data_array;
}, 10 );

When you mutate underlying data, bump my_plugin_data_changed_at to invalidate.


Hook involved

Hook Type Role
mam_main_allow_cache_json_elements Filter Global per-element cache opt-in (default false)

Gotchas

  • reset_cursor() is a hammer. It invalidates the user’s entire cache. Use sparingly.
  • null in a section means “no change” in cursor responses, but null in fields means “no value” — the post-pipeline mam_replace_null_with_empty_string() only touches fields, not section roots.
  • Cloning admins always rebuild. If your cache invalidation logic only triggers under cloning conditions, it won’t catch normal-user staleness.
  • The cursor is per-user. A site-wide change requires bumping every user’s cursor — the simplest way is to bump a global “cache version” option that every section’s timestamp comparison includes.
  • No cross-process cursor coordination. Two simultaneous requests from the same device can both build (rare in practice; mobile clients serialize).

  • Phone data pipeline overview
  • Phone data pipeline phases
  • Mobile JSON shape
  • Hook: mam_get_phone_data_before_send

Metadata

Field Value
Article type Plugin Overview
Plugin slug mam-main
Applies to plugin version 2.1.11+
Category Plugin Reference
Audience PHP developer
Last verified 2026-05-02
Contents

    Need Support?

    Can’t find the answer you’re looking for? Don’t worry we’re here to help!