import { Project } from "../models/project"; import { usePrevious } from "./use-previous"; import { useEffect, useState } from "preact/hooks"; import { generate as diff } from "json-merge-patch"; import useDebounce from "./use-debounce"; export interface ServerSyncOptions { projectURL: string refreshInterval: number syncDelay: number } export const defaultOptions = { refreshInterval: 10000, projectURL: `//${window.location.host}/api/v1/projects`, syncDelay: 5000, } export function useServerSync(project: Project, applyServerUpdate: (project: Project) => void, options: ServerSyncOptions = defaultOptions) { options = Object.assign({}, defaultOptions, options); const [ version, setVersion ] = useState(0); const handleAPIResponse = (res: Response) => { // If the project does not yet exist, create it if (res.status === 404) { return createProject(project, options) .then(res => res.json()) .then(result => { setVersion(result.version); }) ; } // In case of conflict, notify of new server version if (res.status === 409) { return res.json().then((result: any) => { applyServerUpdate(result.project); setVersion(result.version); }); } // If the server version is not modified, do nothing if (res.status === 304) { return; } return res.json().then((result: any) => { applyServerUpdate(result.project); setVersion(result.version); }); }; // Force refresh periodically useEffect(() => { const intervalId = window.setInterval(() => { refreshProject(project, version, options).then(handleAPIResponse); }, options.refreshInterval); return () => clearInterval(intervalId); }, [version]); useEffect(() => { const timeoutId =window.setTimeout(() => { console.log('executing debounced patch'); let previousProject: Project|any = usePrevious(project); if (!previousProject) previousProject = {}; // Trigger patch if project has changed const patch = diff(previousProject, project); console.log('generated patch', patch); if (!patch) return; patchProject(project, patch, version, options).then(handleAPIResponse); }, options.syncDelay); return () => clearTimeout(timeoutId); }); } function refreshProject(project: Project, version: number, options: ServerSyncOptions): Promise { return fetch(`${options.projectURL}/${project.id}?version=${version}`, { method: 'GET' }); } function createProject(project: Project, options: ServerSyncOptions): Promise { return fetch(`${options.projectURL}/${project.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ project }) }); } function patchProject(project: Project, patch: Object, version: number, options: ServerSyncOptions): Promise { return fetch(`${options.projectURL}/${project.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ version, patch }) }); };