Files
BarangaySystem/resources/js/composables/useSyncData.js
2026-06-06 18:43:00 +08:00

124 lines
3.6 KiB
JavaScript

// resources/js/composables/useSyncData.js
import { onMounted, onUnmounted } from 'vue';
import { useSyncStore } from '../stores/syncState.js';
/**
* Composable to handle real-time data synchronization using SSE or polling.
*/
export function useSyncData() {
const syncStore = useSyncStore();
let eventSource = null;
const intervals = {};
/**
* Start Server-Sent Events listener.
* @param {string} endpoint
*/
const startSSE = (endpoint = '/api/sync/events') => {
if (typeof EventSource === 'undefined') {
console.warn('[Sync] EventSource not supported by browser.');
return false;
}
if (eventSource) eventSource.close();
eventSource = new EventSource(endpoint);
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
handleSyncMessage(data);
} catch (e) {
console.error('[Sync] Failed to parse SSE message:', e);
}
};
eventSource.onerror = (err) => {
console.error('[Sync] EventSource connection error, closing.');
eventSource.close();
eventSource = null;
};
return true;
};
/**
* Handle incoming synchronization messages.
* @param {object} payload
*/
const handleSyncMessage = (payload) => {
const { type, key, data } = payload;
syncStore.updateStatus(key || type, 'synced');
// Notify Pinia or other stores about updates
// For example:
// switch (type) {
// case 'user_update':
// useUserStore().fetchCurrentUser();
// break;
// }
};
/**
* Start interval-based polling as a fallback or for specific keys.
* @param {string} key
* @param {string} url
* @param {number} intervalMs
* @param {string} method
* @param {object|null} data
*/
const startPolling = (key, url, intervalMs = 60000, method = 'GET', data = null) => {
if (intervals[key]) return;
const pollFunc = async () => {
try {
syncStore.updateStatus(key, 'syncing');
const options = {
method: method,
headers: { 'Accept': 'application/json' }
};
if (data && (method === 'POST' || method === 'PUT')) {
options.headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(data);
}
const response = await fetch(url, options);
if (!response.ok) throw new Error(`Poll failed: ${response.status}`);
const result = await response.json();
handleSyncMessage({ type: 'poll_result', key, data: result });
} catch (err) {
syncStore.setError(key, err.message);
}
};
// Run immediately then start interval
pollFunc();
intervals[key] = setInterval(pollFunc, intervalMs);
};
/**
* Stop polling for a specific key.
* @param {string} key
*/
const stopPolling = (key) => {
if (intervals[key]) {
clearInterval(intervals[key]);
delete intervals[key];
}
};
// Cleanup on unmount
onUnmounted(() => {
if (eventSource) eventSource.close();
Object.values(intervals).forEach(clearInterval);
});
return {
startSSE,
startPolling,
stopPolling
};
}