Я создаю веб-приложение для отображения на своем iPad, чтобы управлять моим Raspberry Pi, выступающим в качестве диктофона. Частично необходимо поддерживать открытый источник событий, чтобы сервер мог отправлять события на стороне сервера. Определенный c экземпляр приложения может захватить контроль над процессом записи, но потеряет контроль, если сервер увидит, что sse-ссылка закрывается. Это всего лишь защита от исчезновения и оставления контроля над клиентом (контроль над процессом необходимо обновлять, по крайней мере, каждые 5 минут - но я действительно не хочу ждать так долго в обычном случае, когда кто-то просто закрывает браузер вкладка.)
Отчасти мне нужно перевести браузер sh в фоновый режим, чтобы я мог открыть камеру и записать видео.
Я собрал это приложение и получил его почти работает, см. https://github.com/akc42/pi_record.git (основная ветка).
Пока я не переместил браузер в фоновый режим и не обнаружил, что IOS закрыл страницу и сломал ссылку sse.
I попытался реструктурировать с использованием частного веб-работника для управления ссылками sse, скопив сообщения между веб-работником и основным потоком javascript - снова почти работающий (см. раздел «Работники» вышеупомянутого репозитория). Но это тоже отключилось!
Моя последняя мысль - использовать сервисного работника, но как структурировать приложение?
Очевидно, что работник службы должен выступать в качестве клиента для сервера для событий на стороне сервера. Он должен держать соединение открытым, но он также должен отслеживать несколько вкладок в браузере, которые могут или не могут пытаться захватить контроль над интерфейсом, и позволяют делать это только одной вкладке.
Я могу Подумайте о трех подходах - но трудно понять, что лучше. По крайней мере, я никогда не видел упоминаний о подходе 2 и 3 ниже, но мне кажется, что один из этих двух может быть самым простым.
Подход 1
Переместите код, который у меня сейчас есть, для отдельных веб-работников в сервисный работник. Однако нам нужно добавить к сообщению, передающему некоторую форму идентификатора между окном и сервисом. Поэтому я могу записать, какая вкладка фактически захватила контроль над интерфейсом и, следовательно, исключить другие вкладки из этого (ie имитирует неудачную попытку получить контроль).
Насколько я могу понять MessageEvent.ports [ 0] может быть уникальным объектом, который я мог бы хранить где-нибудь на карте, но я не совсем уверен, что MessageChannel не закроется, если браузер переместится в фоновый режим.
Подход 2
имеет набор фантомных URL-адресов в сервисном работнике, которые имитируют все различные типы сообщений (и параметры), которые ранее отправляли мою вкладку своему частному веб-работнику.
Событие выборки предоставляет клиентский код (который я могу использовать для различия между тем, кто на самом деле захватил контроль) и который я могу использовать для выполнения Clients.get(clientid).postMessage()
(или Clients.matchAll
, когда требуется широковещательный ответ)
Код будет выглядеть примерно так
self.addEventListener('fetch', (event) => {
const requestURL = new URL(event.request.url);
if (/^\/api\//.test(requestURL.pathname)) {
event.respondWith(fetch(event.request)); //all api requests are a direct pass through
} else if (/^\/service\//.test(requestURL.pathname)) {
/*
process these like a message passing with one extra to say the client is going away.
*/
if (urlRecognised) {
event.respondWith(new Response('OK', {status: 200}));
} else {
event.respondWith(new Response(`Unknown request ${requestURL.pathname}`, {status: 404}));
}
} else {
event.respondWith(async () => {
const cache = await caches.open('recorder');
const cachedResponse = await cache.match(event.request);
const networkResponsePromise = fetch(event.request);
event.waitUntil(async () => {
const networkResponse = await networkResponsePromise;
await cache.put(event.request, networkResponse.clone());
});
// Returned the cached response if we have one, otherwise return the network response.
return cachedResponse || networkResponsePromise;
});
}
});
Вершина события fetch просто передает стандартные запросы API, сделанные клиентом напрямую. Я не могу их кэшировать (хотя я мог бы быть более изощренным и, возможно, заранее отвергать те, которые не поддерживаются). Второй раздел соответствует фантомным URL-адресам /service/something
Последний раздел взят из автономной кулинарной книги Джейка Арчибальда и пытается использовать кеш, но обновляет кеш в фоновом режиме, если какой-либо из файлов stati c изменился .
Подход 3
Аналогично подходу, описанному выше, в котором мы будем использовать фантомные URL и использовать клиентку в качестве уникального маркера, но на самом деле попытаемся смоделировать сервер поток побочных событий с одним URL.
Я думаю, что код будет больше похож на
...
} else if (/^\/service\//.test(requestURL.pathname)) {
const stream = new TransformStream();
const writer = stream.writeable.getWriter();
event.respondWith(async () => {
const streamFinishedPromise = new Promise(async (resolve,reject) => {
event.waitUntil(async () => {
/* eventually close the link */
await streamFinishedPromise;
});
try {
while (true) writer.write(await nextMessageFromServerSideEventStream());
} catch(e) {
writer.close();
resolve();
}
});
return new Response(stream.readable,{status:200}) //probably need eventstream headers too
}
Я думаю, что подход 2 может быть самым простым, учитывая где я сейчас нахожусь, но меня беспокоит, что я ничего не вижу при поиске того, как использовать сервисных работников, которые обсуждают этот подход с фантомным URL.
Может ли кто-нибудь прокомментировать любой из этих подходов и дать рекомендации о том, как лучше всего запрограммировать хитрые биты (например, закрывается ли канал сообщения подхода 1, когда браузер перемещается в фоновый режим на iPad, или как вы действительно сохраняете Канал ответа открыт, и закрывается ли он, когда браузер переходит на задний план в подходе 3)