Files
Xboard/app/Services/NodeSyncService.php
xboard 010275b09e feat: introduce WebSocket sync for XBoard nodes
- Implement Workerman-based `xboard:ws-server` for real-time node synchronization.
- Support custom routes, outbounds, and certificate configurations via JSON.
- Optimize scheduled tasks with `lazyById` to minimize memory footprint.
- Enhance reactivity using Observers for `Plan`, `Server`, and `ServerRoute`.
- Expand protocol support for `httpupgrade`, `h2`, and `mieru`.
2026-03-15 09:49:11 +08:00

144 lines
4.0 KiB
PHP

<?php
namespace App\Services;
use App\Models\Server;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
class NodeSyncService
{
/**
* Check if node has active WS connection
*/
private static function isNodeOnline(int $nodeId): bool
{
return (bool) Cache::get("node_ws_alive:{$nodeId}");
}
/**
* Push node config update
*/
public static function notifyConfigUpdated(int $nodeId): void
{
if (!self::isNodeOnline($nodeId))
return;
$node = Server::find($nodeId);
if (!$node)
return;
self::push($nodeId, 'sync.config', ['config' => ServerService::buildNodeConfig($node)]);
}
/**
* Push all users to all nodes in the group
*/
public static function notifyUsersUpdatedByGroup(int $groupId): void
{
$servers = Server::whereJsonContains('group_ids', (string) $groupId)
->get();
foreach ($servers as $server) {
if (!self::isNodeOnline($server->id))
continue;
$users = ServerService::getAvailableUsers($server)->toArray();
self::push($server->id, 'sync.users', ['users' => $users]);
}
}
/**
* Push user changes (add/remove) to affected nodes
*/
public static function notifyUserChanged(User $user): void
{
if (!$user->group_id)
return;
$servers = Server::whereJsonContains('group_ids', (string) $user->group_id)->get();
foreach ($servers as $server) {
if (!self::isNodeOnline($server->id))
continue;
if ($user->isAvailable()) {
self::push($server->id, 'sync.user.delta', [
'action' => 'add',
'users' => [
[
'id' => $user->id,
'uuid' => $user->uuid,
'speed_limit' => $user->speed_limit,
'device_limit' => $user->device_limit,
]
],
]);
} else {
self::push($server->id, 'sync.user.delta', [
'action' => 'remove',
'users' => [['id' => $user->id]],
]);
}
}
}
/**
* Push user removal from a specific group's nodes
*/
public static function notifyUserRemovedFromGroup(int $userId, int $groupId): void
{
$servers = Server::whereJsonContains('group_ids', (string) $groupId)
->get();
foreach ($servers as $server) {
if (!self::isNodeOnline($server->id))
continue;
self::push($server->id, 'sync.user.delta', [
'action' => 'remove',
'users' => [['id' => $userId]],
]);
}
}
/**
* Full sync: push config + users to a node
*/
public static function notifyFullSync(int $nodeId): void
{
if (!self::isNodeOnline($nodeId))
return;
$node = Server::find($nodeId);
if (!$node)
return;
self::push($nodeId, 'sync.config', ['config' => ServerService::buildNodeConfig($node)]);
$users = ServerService::getAvailableUsers($node)->toArray();
self::push($nodeId, 'sync.users', ['users' => $users]);
}
/**
* Publish a push command to Redis — picked up by the Workerman WS server
*/
private static function push(int $nodeId, string $event, array $data): void
{
try {
Redis::publish('node:push', json_encode([
'node_id' => $nodeId,
'event' => $event,
'data' => $data,
]));
} catch (\Throwable $e) {
Log::warning("[NodePush] Redis publish failed: {$e->getMessage()}", [
'node_id' => $nodeId,
'event' => $event,
]);
}
}
}