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 versionbutton_array_<role>— per-role button definitionshome_screen_stack— layoutnotification_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.nullin a section means “no change” in cursor responses, butnullin fields means “no value” — the post-pipelinemam_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).
Related articles
- 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 |
