Streaming Mode
Gale's streaming mode allows you to send updates to the client in real-time during long-running operations. Events are sent immediately as they're added, enabling live progress updates, logs, and incremental data loading.
Basic Streaming
Use stream() to wrap your long-running operation. Inside the callback,
all state updates and events are sent immediately to the client.
public function processUsers()
{
return gale()->stream(function ($gale) {
$users = User::all();
$total = $users->count();
$processed = 0;
foreach ($users as $user) {
$user->processExpensiveOperation();
$processed++;
// Sent immediately to client
$gale->state('progress', [
'current' => $processed,
'total' => $total,
'percent' => round(($processed / $total) * 100),
]);
}
$gale->state('complete', true);
$gale->messages(['_success' => "Processed {$total} users"]);
});
}
public function processUsers()
{
return gale()->stream(function ($gale) {
$users = User::all();
$total = $users->count();
$processed = 0;
foreach ($users as $user) {
$user->processExpensiveOperation();
$processed++;
// Sent immediately to client
$gale->state('progress', [
'current' => $processed,
'total' => $total,
'percent' => round(($processed / $total) * 100),
]);
}
$gale->state('complete', true);
$gale->messages(['_success' => "Processed {$total} users"]);
});
}
Frontend Implementation
The frontend receives updates in real-time. Use Alpine.js to display progress:
<div
x-data="{
progress: { current: 0, total: 0, percent: 0 },
complete: false
}">
<button @click="$postx('/process-users')" x-loading.attr="disabled">
<span x-loading.remove>Start Processing</span>
<span x-loading>Processing...</span>
</button>
<!-- Live progress bar -->
<div x-show="progress.total > 0 && !complete" class="mt-4">
<div class="flex justify-between mb-1">
<span x-text=`Processing ${progress.current} of ${progress.total}`></span>
<span x-text="progress.percent + '%'"></span>
</div>
<div class="h-3 bg-gray-200 rounded-full overflow-hidden">
<div
class="h-full bg-blue-500 transition-all duration-150"
:style="'width: ' + progress.percent + '%'"></div>
</div>
</div>
<!-- Success message -->
<div x-message="_success" class="mt-4 p-4 bg-green-100 text-green-700 rounded"></div>
</div>
<div
x-data="{
progress: { current: 0, total: 0, percent: 0 },
complete: false
}">
<button @click="$postx('/process-users')" x-loading.attr="disabled">
<span x-loading.remove>Start Processing</span>
<span x-loading>Processing...</span>
</button>
<!-- Live progress bar -->
<div x-show="progress.total > 0 && !complete" class="mt-4">
<div class="flex justify-between mb-1">
<span x-text="`Processing ${progress.current} of ${progress.total}`"></span>
<span x-text="progress.percent + '%'"></span>
</div>
<div class="h-3 bg-gray-200 rounded-full overflow-hidden">
<div
class="h-full bg-blue-500 transition-all duration-150"
:style="'width: ' + progress.percent + '%'"></div>
</div>
</div>
<!-- Success message -->
<div x-message="_success" class="mt-4 p-4 bg-green-100 text-green-700 rounded"></div>
</div>
Accumulation vs Streaming Mode
In normal mode, Gale accumulates all events and sends them at the end of the request. In streaming mode, events are sent immediately as they're added.
| Feature | Normal Mode | Streaming Mode |
|---|---|---|
| Event delivery | All at once (end of request) | Immediately as added |
| Use case | Quick operations | Long-running tasks |
| Progress updates | Not visible | Real-time |
| Connection | Single response | Kept open |
| Memory usage | Events buffered | Events sent & freed |
Pre-Stream Events
Events added before stream() is called are flushed when streaming begins:
return gale()
// These events are sent when streaming starts
->state('status', 'initializing')
->state('startedAt', now())
// Then streaming mode begins
->stream(function ($gale) {
foreach ($items as $item) {
$gale->state('status', "Processing {$item->name}");
// Process item...
}
$gale->state('status', 'complete');
});
return gale()
// These events are sent when streaming starts
->state('status', 'initializing')
->state('startedAt', now())
// Then streaming mode begins
->stream(function ($gale) {
foreach ($items as $item) {
$gale->state('status', "Processing {$item->name}");
// Process item...
}
$gale->state('status', 'complete');
});
Memory-Efficient Processing
Use Laravel's cursor() for memory-efficient processing of large datasets:
return gale()->stream(function ($gale) {
// Cursor uses lazy loading - only one model in memory at a time
$users = User::cursor();
$total = User::count();
$processed = 0;
foreach ($users as $user) {
$user->expensiveOperation();
$processed++;
// Update every 10 records to reduce network overhead
if ($processed % 10 === 0) {
$gale->state('progress', [
'current' => $processed,
'total' => $total,
]);
}
}
$gale->state('complete', true);
});
return gale()->stream(function ($gale) {
// Cursor uses lazy loading - only one model in memory at a time
$users = User::cursor();
$total = User::count();
$processed = 0;
foreach ($users as $user) {
$user->expensiveOperation();
$processed++;
// Update every 10 records to reduce network overhead
if ($processed % 10 === 0) {
$gale->state('progress', [
'current' => $processed,
'total' => $total,
]);
}
}
$gale->state('complete', true);
});
Exception Handling
Exceptions in streaming mode are automatically captured and displayed with full stack traces. The error is rendered as a native Laravel exception page via SSE.
return gale()->stream(function ($gale) {
foreach ($items as $item) {
$gale->state('current', $item->id);
// If an exception occurs, Gale renders it and sends to browser
if ($item->isFailed()) {
throw new \Exception("Failed to process item {$item->id}");
}
$item->process();
}
});
// Or handle exceptions gracefully
return gale()->stream(function ($gale) {
$errors = [];
foreach ($items as $item) {
try {
$item->process();
} catch (\Exception $e) {
$errors[] = $e->getMessage();
$gale->state('errors', $errors);
}
}
if (count($errors) > 0) {
$gale->messages(['_warning' => count($errors) . ' items failed']);
}
});
return gale()->stream(function ($gale) {
foreach ($items as $item) {
$gale->state('current', $item->id);
// If an exception occurs, Gale renders it and sends to browser
if ($item->isFailed()) {
throw new \Exception("Failed to process item {$item->id}");
}
$item->process();
}
});
// Or handle exceptions gracefully
return gale()->stream(function ($gale) {
$errors = [];
foreach ($items as $item) {
try {
$item->process();
} catch (\Exception $e) {
$errors[] = $e->getMessage();
$gale->state('errors', $errors);
}
}
if (count($errors) > 0) {
$gale->messages(['_warning' => count($errors) . ' items failed']);
}
});
Debugging with dd() and dump()
Laravel's dd() and dump() work in streaming mode. Output is
captured and displayed in the browser.
return gale()->stream(function ($gale) {
$users = User::with('orders')->get();
// dump() output is captured and sent via SSE
dump($users->first());
foreach ($users as $user) {
// Debugging in the middle of processing
if ($user->id === 42) {
dd($user, $user->orders); // Stops execution, shows in browser
}
$gale->state('processed', $user->id);
}
});
return gale()->stream(function ($gale) {
$users = User::with('orders')->get();
// dump() output is captured and sent via SSE
dump($users->first());
foreach ($users as $user) {
// Debugging in the middle of processing
if ($user->id === 42) {
dd($user, $user->orders); // Stops execution, shows in browser
}
$gale->state('processed', $user->id);
}
});
Redirects in Streaming Mode
Redirects work in streaming mode via JavaScript. When you call Laravel's redirect helpers, Gale intercepts them and performs client-side navigation.
return gale()->stream(function ($gale) {
foreach ($items as $item) {
$gale->state('processing', $item->id);
$item->process();
}
// These all work in streaming mode
return redirect('/dashboard');
// return redirect()->route('users.index');
// return redirect()->back();
});
return gale()->stream(function ($gale) {
foreach ($items as $item) {
$gale->state('processing', $item->id);
$item->process();
}
// These all work in streaming mode
return redirect('/dashboard');
// return redirect()->route('users.index');
// return redirect()->back();
});
Live Logging
Stream logs to the browser in real-time during processing:
return gale()->stream(function ($gale) {
$logs = [];
$log = function($message) use ($gale, &$logs) {
$logs[] = [
'time' => now()->format('H:i:s'),
'message' => $message,
];
$gale->state('logs', $logs);
};
$log('Starting import...');
foreach ($records as $record) {
$log("Importing record {$record->id}");
$record->import();
}
$log('Import complete!');
});
return gale()->stream(function ($gale) {
$logs = [];
$log = function($message) use ($gale, &$logs) {
$logs[] = [
'time' => now()->format('H:i:s'),
'message' => $message,
];
$gale->state('logs', $logs);
};
$log('Starting import...');
foreach ($records as $record) {
$log("Importing record {$record->id}");
$record->import();
}
$log('Import complete!');
});
<!-- Live log display -->
<div x-data="{ logs: [] }" class="max-h-64 overflow-y-auto bg-gray-900 rounded p-4">
<template x-for="log in logs" :key="log.time + log.message">
<div class="text-sm font-mono text-green-400">
<span class="text-gray-500" x-text="'[' + log.time + ']'"></span>
<span x-text="log.message"></span>
</div>
</template>
</div>
<!-- Live log display -->
<div x-data="{ logs: [] }" class="max-h-64 overflow-y-auto bg-gray-900 rounded p-4">
<template x-for="log in logs" :key="log.time + log.message">
<div class="text-sm font-mono text-green-400">
<span class="text-gray-500" x-text="'[' + log.time + ']'"></span>
<span x-text="log.message"></span>
</div>
</template>
</div>
Streaming Features Summary
| Feature | Behavior |
|---|---|
| Events | Sent immediately as added |
| dd() / dump() | Output captured and displayed in browser |
| Exceptions | Rendered with full stack traces |
| Redirects | Work via JavaScript navigation |
| Pre-stream events | Flushed when streaming begins |
| Connection | Kept open until callback completes |