@budarin/pluggable-serviceworker 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,39 +6,59 @@
6
6
 
7
7
  ## 🚀 Почему этот пакет облегчает разработку?
8
8
 
9
- Разработка Service Worker'ов традиционно сложна из-за необходимости вручную управлять множественными обработчиками событий, обработкой ошибок и порядком выполнения. Этот пакет решает эти проблемы:
9
+ Разработка Service Worker'ов традиционно сложна из-за необходимости вручную управлять множественными обработчиками событий, обработкой ошибок и порядком выполнения или изучать сложные и большие библиотеки. Этот пакет решает эти проблемы:
10
10
 
11
11
  ### 🔌 **Модульная архитектура**
12
12
 
13
13
  - **Плагинная система** позволяет разбивать функциональность на независимые модули
14
14
  - Каждый плагин отвечает за свою задачу (кеширование, аутентификация, уведомления)
15
15
  - Легко добавлять/удалять функциональность без изменения основного кода
16
- - Не нужно думать об инфраструктурном коде в обработчиках событий - пишите простой код не думая о сложностях кода самого сервисворкера
16
+ - Не нужно думать об инфраструктурном коде в обработчиках событий пишите простой код, не думая о сложностях самого сервис-воркера
17
17
 
18
- ### 🎯 **Управление порядком выполнения**
18
+ ### 🎯 **Предсказуемый порядок выполнения**
19
19
 
20
- - **Предсказуемый порядок** - плагины без `order` выполняются первыми, затем по возрастанию `order`
21
- - **Гибкость** - можно контролировать последовательность инициализации
22
- - **Масштабируемость** - легко добавлять новые плагины в нужном месте
20
+ - **Предсказуемый порядок** плагины без `order` выполняются первыми, затем по возрастанию/убыванию `order`
21
+ - **Гибкость** можно контролировать последовательность инициализации
22
+ - **Параллельно** для `install`, `activate`, `message`, `sync` независимые задачи выполняются одновременно
23
+ - **Последовательно** для `fetch` — первый вернувший ответ завершает цепочку; для `push` вызываются все плагины
24
+ - Легко добавлять новые плагины в нужное место
23
25
 
24
- ### **Оптимизированная логика выполнения**
26
+ ### 📖 **Легко изучить и понять**
25
27
 
26
- - **Параллельно** для `install`, `activate`, `message`, `sync` - независимые задачи выполняются одновременно
27
- - **Последовательно** для `fetch` первый успешный результат прерывает цепочку; для `push` — вызываются все плагины, показывается уведомление по каждому payload (или одно от библиотеки только когда **все** плагины вернули `undefined`)
28
- - **Производительность** - правильный выбор стратегии для каждого типа события
28
+ - Один контракт плагин с опциональными хуками, без отдельной модели роутинга и стратегий
29
+ - Мало сущностей: плагин, фабрика плагина, `initServiceWorker`, опции
30
+ - Низкая когнитивная нагрузка: цепочка плагинов и возвращаемые значения задают всё поведение
31
+ - Быстро войти в проект по примерам и типу `ServiceWorkerPlugin`
32
+
33
+ ### 📦 **Маленький размер**
34
+
35
+ - Минимум зависимостей, только рантайм плагинов
36
+ - Нет встроенной сборки и тяжёлых модулей — в бандл попадает только то, что вы подключаете
37
+ - Подходит для проектов, где важен размер бандла и простота зависимостей
38
+
39
+ ### 🎛 **Полный контроль над кодом**
40
+
41
+ - Список кешируемых ассетов и стратегии задаёте вы — в коде или конфиге
42
+ - Порядок плагинов, обработка ошибок и логгер полностью в ваших руках
43
+ - Любая кастомная логика в `fetch`, `push`, `sync` — через свои плагины, без обхода чужих абстракций
29
44
 
30
45
  ### 🛡️ **Централизованная обработка ошибок**
31
46
 
32
- - **Единый обработчик** для всех типов ошибок
33
- - **Типизированные ошибки** - знаешь, что именно сломалось
34
- - **Изоляция** - ошибка в одном плагине не ломает остальные
47
+ - **Единый обработчик** `onError` для всех типов ошибок в сервис-воркере с определение места возникновения ошибки в коде
48
+ - **Типизированные ошибки** знаешь, что именно сломалось
49
+ - **Изоляция** ошибка в одном плагине не ломает остальные
35
50
  - **Автоматическая обработка** глобальных событий ошибок
36
51
 
37
52
  ### 📝 **Удобное логирование**
38
53
 
39
- - **Настраиваемый логгер** с разными уровнями
40
- - **Контекстная информация** в логах
41
- - **Отладка** становится намного проще
54
+ - **Настраиваемый логгер** с разными уровнями (`trace`, `debug`, `info`, `warn`, `error`)
55
+ - Во все обработчики плагинов передаётся один и тот же `logger` — контекстная отладка
56
+ - При желании можно подставить любой объект с этим интерфейсом
57
+
58
+ ### ✅ **Готовые решения из коробки**
59
+
60
+ - Пресет **offlineFirst** — precache при установке и отдача из кеша при запросах
61
+ - Готовые сервисворкеры: **activateOnSignal**, **activateImmediately**, **activateOnNextVisit** — рабочий типовой сервис-воркер в несколько строк без написания плагинов
42
62
 
43
63
  ## 📦 Установка
44
64
 
@@ -440,6 +460,7 @@ function authPlugin(config: {
440
460
  if (protectedPaths.some((p) => path.startsWith(p))) {
441
461
  if (needsAuth(event.request)) {
442
462
  logger.warn('auth: unauthorized', event.request.url);
463
+
443
464
  return new Response('Unauthorized', { status: 401 }); // Прерывает цепочку
444
465
  }
445
466
  }
@@ -454,7 +475,7 @@ function authPlugin(config: {
454
475
  **Почему последовательно:**
455
476
 
456
477
  - **fetch**: Нужен только один ответ на текущий запрос браузера, первый успешный прерывает цепочку. Если никто не вернул ответ — выполняется `fetch(event.request)`
457
- - **push**: Плагин может вернуть `PushNotificationPayload`, `false` (не показывать) или `undefined` (решение отдаётся библиотеке). Библиотека вызывает `showNotification` для каждого payload. Не показываем, если все вернули `false` или смесь без payload. Библиотека показывает одно только когда **все** вернули `undefined`.
478
+ - **push**: Плагин может вернуть `PushNotificationPayload`, `false` (не показывать) или `undefined` (решение отдаётся библиотеке). Библиотека вызывает `showNotification` для каждого payload. Не показываем, если все вернули `false` или смесь без payload. Библиотека показывает нотификацию и в случае когда **все** плагины вернули `undefined`.
458
479
 
459
480
  ### 📋 Сводная таблица
460
481
 
@@ -479,19 +500,19 @@ function authPlugin(config: {
479
500
  | Название | Событие | Описание |
480
501
  | ------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------- |
481
502
  | `precache*(config)` | `install` | Кеширует список ресурсов из `config.assets` в кеш `config.cacheName`. |
482
- | `precacheAndNotify(config)` | install | Как **precache**, затем отправляет активным клиентам сообщение `{ type: config.messageType }` (по умолчанию `SW_INSTALLED`). |
503
+ | `precacheAndNotify(config)` | `install` | Как **precache**, затем отправляет активным клиентам сообщение `{ type: config.messageType }` (по умолчанию `SW_INSTALLED`). |
483
504
  | `precacheMissing(config)` | `install` | Добавляет в кеш только те ресурсы из `config.assets`, которых ещё нет в кеше. |
484
505
  | `pruneStaleCache(config)` | `activate` | Удаляет из кеша записи, чей URL не входит в `config.assets`. |
485
- | `skipWaiting | `install` | Вызывает `skipWaiting()`. |
506
+ | `skipWaiting` | `install` | Вызывает `skipWaiting()`. |
486
507
  | `claim` | `activate` | Вызывает `clients.claim()`. |
487
508
  | `reloadClients` | `activate` | Перезагружает все окна-клиенты через `client.navigate(client.url)`. |
488
509
  | `claimAndReloadClients` | `activate` | Композиция **claim** + **reloadClients**: сначала claim, затем перезагрузка (порядок гарантирован — один плагин). |
489
510
  | `skipWaitingOnMessage(config?)` | `message` | При сообщении с `event.data.type === 'SW_MSG_SKIP_WAITING'` вызывает `skipWaiting()`. |
490
511
  | `serveFromCache(config)` | `fetch` | Отдаёт ресурс из кеша `config.cacheName`; при отсутствии его в кэше — undefined. |
491
512
  | `restoreAssetToCache(config)` | `fetch` | Для URL из `config.assets`: отдам ресурс из кеша или запрашиваем по сети, затем в кладем кго в кеш. Иначе — undefined. |
492
- | cacheFirst(config)` | `fetch` | Отдаем ресурс из кэша `config.cacheName`: при отсутствии его в кэше — делаем запрос на сервер и затем кладем ответ в кэш. |
513
+ | `cacheFirst(config)` | `fetch` | Отдаем ресурс из кэша `config.cacheName`: при отсутствии его в кэше — делаем запрос на сервер и затем кладем ответ в кэш. |
493
514
  | `networkFirst(config)` | `fetch` | Делаем запрос на сервер, при успехе — кладем его в кэш. При ошибке — отдаем из кэша. Иначе - `undefined`. |
494
- | `staleWhileRevalidate(config)` | fetch | Отдаёт из кэша, в фоне обновляет кэш. |
515
+ | `staleWhileRevalidate(config)` | `fetch` | Отдаёт из кэша, в фоне обновляет кэш. |
495
516
 
496
517
  #### Композиция примитивов
497
518
 
@@ -499,32 +520,29 @@ function authPlugin(config: {
499
520
 
500
521
  **Пример: claimAndReloadClients как композиция двух примитивов**
501
522
 
502
- Плагин **claimAndReloadClients** из коробки выполняет в `activate` последовательно: `clients.claim()`, затем перезагрузка всех окон.
523
+ Плагин **claimAndReloadClients** вызывает существующие примитивы **claim** и **reloadClients** по очереди:
503
524
 
504
525
  ```typescript
505
- // Реализация плагина claimAndReloadClients (из @budarin/pluggable-serviceworker/plugins)
506
- activate: () =>
507
- self.clients.claim().then(() =>
508
- self.clients
509
- .matchAll({ type: 'window' })
510
- .then((list) =>
511
- Promise.all(list.map((client) => client.navigate(client.url)))
512
- )
513
- ),
514
- ```
526
+ import { claim } from '@budarin/pluggable-serviceworker/plugins';
527
+ import { reloadClients } from '@budarin/pluggable-serviceworker/plugins';
515
528
 
516
- Два плагина **claim** и **reloadClients** в стеке выполняют `activate` **параллельно** (см. «Логика выполнения обработчиков»), порядок завершения операций не гарантирован. Для строгой последовательности «сначала claim, потом reload» используйте один плагин **claimAndReloadClients**.
529
+ const claimPlugin = claim();
530
+ const reloadPlugin = reloadClients();
517
531
 
518
- ```typescript
519
- initServiceWorker([claim, reloadClients], options); // оба activate — параллельно
532
+ activate: (event, logger) =>
533
+ Promise.resolve(claimPlugin.activate(event, logger)).then(() =>
534
+ reloadPlugin.activate(event, logger)
535
+ ),
520
536
  ```
521
537
 
522
538
  **Пример: кастомный кэш и логика по URL**
523
539
 
524
- Фабрика `postsSwrPlugin(config)` возвращает плагин, который применяет stale-while-revalidate только к запросам, подходящим под `pathPattern`. Логика SWR инлайнована в обработчик `fetch`.
540
+ Фабрика `postsSwrPlugin(config)` возвращает плагин, который применяет `stale-while-revalidate`(SWR) только к запросам, подходящим под `pathPattern`.
525
541
 
526
542
  ```typescript
543
+ // postsSwrPlugin.ts
527
544
  import type { Plugin } from '@budarin/pluggable-serviceworker';
545
+
528
546
  import {
529
547
  precache,
530
548
  serveFromCache,
@@ -536,28 +554,39 @@ function postsSwrPlugin(config: {
536
554
  pathPattern?: RegExp;
537
555
  }): Plugin {
538
556
  const { cacheName, pathPattern = /\/api\/posts(\/|$)/ } = config;
557
+
539
558
  return {
540
559
  name: 'postsSwr',
541
560
  order: 0,
561
+
542
562
  fetch: async (event) => {
543
- if (!pathPattern.test(new URL(event.request.url).pathname))
563
+ if (!pathPattern.test(new URL(event.request.url).pathname)) {
544
564
  return undefined;
565
+ }
566
+
545
567
  const cache = await caches.open(cacheName);
546
568
  const cached = await cache.match(event.request);
547
569
  const revalidate = fetch(event.request).then(async (response) => {
548
- if (response.ok)
570
+ if (response.ok) {
549
571
  await cache.put(event.request, response.clone());
572
+ }
573
+
550
574
  return response;
551
575
  });
576
+
552
577
  if (cached) {
553
578
  void revalidate;
554
579
  return cached;
555
580
  }
581
+
556
582
  return revalidate;
557
583
  },
558
584
  };
559
585
  }
586
+ ```
560
587
 
588
+ ```typescript
589
+ // sw.ts
561
590
  const staticCache = 'static-v1';
562
591
  const assets = ['/', '/main.js'];
563
592
 
@@ -580,7 +609,6 @@ initServiceWorker(
580
609
  | `offlineFirst(config)` | `precache(config) + serveFromCache(config)` | Статика из кеша, при отсутствии ресурса в кэше — делаем запрос к серверу. |
581
610
 
582
611
  Конфиг пресета: `OfflineFirstConfig` (cacheName, assets). Импорт из `@budarin/pluggable-serviceworker/presets`.
583
-
584
612
  Стратегии **networkFirst**, **staleWhileRevalidate** и др. доступны как примитивы — собирайте свой кастомный сервис-воркер из примитивов и пресетов.
585
613
 
586
614
  ### Типовые сервис-воркеры (из коробки)
@@ -1,10 +1,10 @@
1
+ import { claim } from './claim.js';
2
+ import { reloadClients } from './reloadClients.js';
1
3
  export function claimAndReloadClients() {
4
+ const claimPlugin = claim();
5
+ const reloadPlugin = reloadClients();
2
6
  return {
3
7
  name: 'claimAndReloadClients',
4
- activate: () => self.clients
5
- .claim()
6
- .then(() => self.clients
7
- .matchAll({ type: 'window' })
8
- .then((list) => Promise.all(list.map((client) => client.navigate(client.url))))),
8
+ activate: (event, logger) => Promise.resolve(claimPlugin.activate(event, logger)).then(() => reloadPlugin.activate(event, logger)),
9
9
  };
10
10
  }
@@ -4,8 +4,9 @@ export function skipWaitingOnMessage(config = {}) {
4
4
  return {
5
5
  name: 'skipWaitingOnMessage',
6
6
  message: (event) => {
7
- if (event.data?.type !== messageType)
7
+ if (event.data?.type !== messageType) {
8
8
  return;
9
+ }
9
10
  self.skipWaiting();
10
11
  },
11
12
  };
@@ -1,5 +1,5 @@
1
1
  import type { ServiceWorkerInitOptions } from '../index.js';
2
- import { type OfflineFirstConfig } from '../presets/offlineFirst.js';
2
+ import type { OfflineFirstConfig } from '../presets/offlineFirst.js';
3
3
  export interface ActivateImmediatelyOptions extends ServiceWorkerInitOptions, OfflineFirstConfig {
4
4
  }
5
5
  export declare function activateImmediatelyServiceWorker(options: ActivateImmediatelyOptions): void;
@@ -1,7 +1,7 @@
1
+ import { claim } from '../plugins/claim.js';
1
2
  import { initServiceWorker } from '../index.js';
2
- import { offlineFirst, } from '../presets/offlineFirst.js';
3
3
  import { skipWaiting } from '../plugins/skipWaiting.js';
4
- import { claim } from '../plugins/claim.js';
4
+ import { offlineFirst } from '../presets/offlineFirst.js';
5
5
  export function activateImmediatelyServiceWorker(options) {
6
6
  initServiceWorker([...offlineFirst(options), skipWaiting, claim], options);
7
7
  }
@@ -1,5 +1,5 @@
1
1
  import type { ServiceWorkerInitOptions } from '../index.js';
2
- import { type OfflineFirstConfig } from '../presets/offlineFirst.js';
2
+ import type { OfflineFirstConfig } from '../presets/offlineFirst.js';
3
3
  export interface ActivateOnNextVisitOptions extends ServiceWorkerInitOptions, OfflineFirstConfig {
4
4
  }
5
5
  export declare function activateOnNextVisitServiceWorker(options: ActivateOnNextVisitOptions): void;
@@ -1,5 +1,5 @@
1
1
  import { initServiceWorker } from '../index.js';
2
- import { offlineFirst, } from '../presets/offlineFirst.js';
2
+ import { offlineFirst } from '../presets/offlineFirst.js';
3
3
  export function activateOnNextVisitServiceWorker(options) {
4
4
  initServiceWorker(offlineFirst(options), options);
5
5
  }
@@ -0,0 +1,4 @@
1
+ export declare function openCacheAndMatch(cacheName: string, request: Request): Promise<{
2
+ cache: Cache;
3
+ cached: Response | undefined;
4
+ }>;
@@ -0,0 +1,5 @@
1
+ export async function openCacheAndMatch(cacheName, request) {
2
+ const cache = await caches.open(cacheName);
3
+ const cached = await cache.match(request);
4
+ return { cache, cached: cached ?? undefined };
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@budarin/pluggable-serviceworker",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "Extensible via plugins service worker",
5
5
  "keywords": [
6
6
  "serviceworker",