Client

Syncing

Run Laravel Tether sync manually, from queues, or on a schedule, and understand push/pull results, conflicts, retries, and pull pagination.

Use Tether to sync offline Laravel client apps on demand, from queue jobs, or on a schedule. Each sync cycle can push local mutations, pull authoritative server snapshots, report conflicts, and keep the local database reconciled.


The TetherClient facade

All sync operations are available on the TetherClient facade:

use Tether\Client\Facades\TetherClient;

$result = TetherClient::sync(); // push, then pull
$result = TetherClient::push(); // push only
$result = TetherClient::pull(); // pull only

SyncResult

All three methods return a Tether\Client\SyncResult object:

$result->pushed;     // int - mutations applied on the server
$result->pulled;     // int - snapshots applied locally
$result->conflicts;  // int - conflicts returned by the server
$result->failed;     // int - rejected mutations
$result->rejections; // list<PushRejection>
$result->pullErrors; // int - snapshots that failed to apply
$result->skipped;    // bool - true if internal Tether lock prevented the cycle (see "Concurrency protection" below)

Use helpers to inspect structured rejections:

$result->rejectionsByReason('validation_failed');
$result->validationErrors();

Transport errors from Laravel's HTTP client calling your server bubble to the caller or queued job handler; they are not stored on SyncResult.


Artisan commands

php artisan tether:sync   # push then pull
php artisan tether:push   # push only
php artisan tether:pull   # pull only
php artisan tether:status # queue counts and last cursor

Scheduling

Schedule automatic sync in routes/console.php or your console kernel:

use Illuminate\Support\Facades\Schedule;

Schedule::command('tether:sync')->everyFiveMinutes();

For mutation-triggered push, enable auto_sync:

// config/tether-client.php
'auto_sync' => true,

auto_sync dispatches PushJob after local model writes. PushJob is push-only, so inbound snapshots do not cause feedback loops.


Queue jobs

Two jobs are available:

use Tether\Client\Jobs\PushJob;
use Tether\Client\Jobs\PullJob;

PushJob::dispatch(); // push only
PullJob::dispatch(); // pull only

Both jobs use auto_sync_queue if set. PushJob can be coalesced with auto_sync_throttle, which uses Laravel's unique job lock to avoid queueing a job for every rapid local write.


How a push cycle works

  1. Failed mutations rejected with reason error are moved back to pending while below max_retry_attempts.
  2. Pending mutations are loaded and chunked into batches of push_batch_size.
  3. Each batch is sent to TETHER_SERVER_PUSH_URL as JSON.
  4. The server responds with applied, rejected, and conflicts.
  5. Applied mutations are marked synced.
  6. Rejected mutations are marked failed.
  7. Conflicted mutations are marked conflict, and the server state is applied locally.

How a pull cycle works

  1. The stored last_sync_cursor is read from tether_sync_state.
  2. A pull request is sent to TETHER_SERVER_PULL_URL with the integer microsecond cursor, or null for a full pull.
  3. The server returns snapshots, new_sync_cursor, and has_more.
  4. Each snapshot is applied locally by SnapshotApplicator using forceFill() and save() inside Model::withoutEvents().
  5. The cursor is persisted in tether_sync_state.
  6. If has_more is true, the cycle repeats with the new cursor until all pages are consumed.

Concurrency protection

All three methods use Laravel's atomic cache lock (tether_sync_lock, 60-second TTL) when sync_lock is enabled. If the lock is already held, the method returns immediately with skipped = true.

Cache driver requirement: Atomic locks require one of: memcached, redis, dynamodb, database, file, or array.