@budarin/pluggable-serviceworker 1.2.1 → 1.5.1

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.
Files changed (98) hide show
  1. package/README.md +347 -159
  2. package/dist/index.d.ts +16 -23
  3. package/dist/index.js +61 -17
  4. package/dist/plugins/cacheFirst.d.ts +5 -3
  5. package/dist/plugins/cacheFirst.js +22 -19
  6. package/dist/plugins/claim.d.ts +2 -3
  7. package/dist/plugins/claim.js +6 -7
  8. package/dist/plugins/claimAndReloadClients.d.ts +2 -0
  9. package/dist/plugins/claimAndReloadClients.js +10 -0
  10. package/dist/plugins/index.d.ts +9 -5
  11. package/dist/plugins/index.js +9 -5
  12. package/dist/plugins/networkFirst.d.ts +5 -3
  13. package/dist/plugins/networkFirst.js +18 -16
  14. package/dist/plugins/precache.d.ts +6 -3
  15. package/dist/plugins/precache.js +10 -8
  16. package/dist/plugins/precacheAndNotify.d.ts +6 -0
  17. package/dist/plugins/precacheAndNotify.js +13 -0
  18. package/dist/plugins/precacheMissing.d.ts +6 -0
  19. package/dist/plugins/precacheMissing.js +16 -0
  20. package/dist/plugins/pruneStaleCache.d.ts +6 -0
  21. package/dist/plugins/pruneStaleCache.js +17 -0
  22. package/dist/plugins/reloadClients.d.ts +2 -0
  23. package/dist/plugins/reloadClients.js +9 -0
  24. package/dist/plugins/restoreAssetToCache.d.ts +6 -3
  25. package/dist/plugins/restoreAssetToCache.js +22 -37
  26. package/dist/plugins/serveFromCache.d.ts +5 -3
  27. package/dist/plugins/serveFromCache.js +10 -11
  28. package/dist/plugins/skipWaiting.d.ts +2 -3
  29. package/dist/plugins/skipWaiting.js +8 -7
  30. package/dist/plugins/skipWaitingOnMessage.d.ts +5 -0
  31. package/dist/plugins/skipWaitingOnMessage.js +13 -0
  32. package/dist/plugins/staleWhileRevalidate.d.ts +5 -3
  33. package/dist/plugins/staleWhileRevalidate.js +20 -18
  34. package/dist/presets/index.d.ts +1 -2
  35. package/dist/presets/index.js +0 -1
  36. package/dist/presets/offlineFirst.d.ts +6 -3
  37. package/dist/presets/offlineFirst.js +3 -5
  38. package/dist/sw/activateImmediately.d.ts +5 -3
  39. package/dist/sw/activateImmediately.js +4 -5
  40. package/dist/sw/activateOnNextVisit.d.ts +5 -3
  41. package/dist/sw/activateOnNextVisit.js +2 -3
  42. package/dist/sw/activateOnSignal.d.ts +6 -3
  43. package/dist/sw/activateOnSignal.js +11 -5
  44. package/dist/sw/index.d.ts +0 -1
  45. package/dist/sw/index.js +0 -1
  46. package/dist/utils/index.d.ts +2 -0
  47. package/dist/utils/index.js +2 -0
  48. package/dist/utils/isAssetRequest.d.ts +1 -0
  49. package/dist/utils/isAssetRequest.js +10 -0
  50. package/dist/utils/isRequestUrlInAssets.d.ts +1 -0
  51. package/dist/utils/isRequestUrlInAssets.js +10 -0
  52. package/dist/utils/normalizeUrl.d.ts +1 -0
  53. package/dist/utils/normalizeUrl.js +3 -0
  54. package/dist/utils/notifyClients.d.ts +1 -0
  55. package/dist/utils/notifyClients.js +4 -0
  56. package/dist/utils/openCacheAndMatch.d.ts +4 -0
  57. package/dist/utils/openCacheAndMatch.js +5 -0
  58. package/package.json +11 -8
  59. package/dist/index.d.ts.map +0 -1
  60. package/dist/index.js.map +0 -1
  61. package/dist/plugins/cacheFirst.d.ts.map +0 -1
  62. package/dist/plugins/cacheFirst.js.map +0 -1
  63. package/dist/plugins/claim.d.ts.map +0 -1
  64. package/dist/plugins/claim.js.map +0 -1
  65. package/dist/plugins/claimOnMessage.d.ts +0 -3
  66. package/dist/plugins/claimOnMessage.d.ts.map +0 -1
  67. package/dist/plugins/claimOnMessage.js +0 -11
  68. package/dist/plugins/claimOnMessage.js.map +0 -1
  69. package/dist/plugins/index.d.ts.map +0 -1
  70. package/dist/plugins/index.js.map +0 -1
  71. package/dist/plugins/networkFirst.d.ts.map +0 -1
  72. package/dist/plugins/networkFirst.js.map +0 -1
  73. package/dist/plugins/networkOnly.d.ts +0 -3
  74. package/dist/plugins/networkOnly.d.ts.map +0 -1
  75. package/dist/plugins/networkOnly.js +0 -5
  76. package/dist/plugins/networkOnly.js.map +0 -1
  77. package/dist/plugins/precache.d.ts.map +0 -1
  78. package/dist/plugins/precache.js.map +0 -1
  79. package/dist/plugins/restoreAssetToCache.d.ts.map +0 -1
  80. package/dist/plugins/restoreAssetToCache.js.map +0 -1
  81. package/dist/plugins/serveFromCache.d.ts.map +0 -1
  82. package/dist/plugins/serveFromCache.js.map +0 -1
  83. package/dist/plugins/skipWaiting.d.ts.map +0 -1
  84. package/dist/plugins/skipWaiting.js.map +0 -1
  85. package/dist/plugins/staleWhileRevalidate.d.ts.map +0 -1
  86. package/dist/plugins/staleWhileRevalidate.js.map +0 -1
  87. package/dist/presets/index.d.ts.map +0 -1
  88. package/dist/presets/index.js.map +0 -1
  89. package/dist/presets/offlineFirst.d.ts.map +0 -1
  90. package/dist/presets/offlineFirst.js.map +0 -1
  91. package/dist/sw/activateImmediately.d.ts.map +0 -1
  92. package/dist/sw/activateImmediately.js.map +0 -1
  93. package/dist/sw/activateOnNextVisit.d.ts.map +0 -1
  94. package/dist/sw/activateOnNextVisit.js.map +0 -1
  95. package/dist/sw/activateOnSignal.d.ts.map +0 -1
  96. package/dist/sw/activateOnSignal.js.map +0 -1
  97. package/dist/sw/index.d.ts.map +0 -1
  98. package/dist/sw/index.js.map +0 -1
package/README.md CHANGED
@@ -24,7 +24,7 @@
24
24
  ### ⚡ **Оптимизированная логика выполнения**
25
25
 
26
26
  - **Параллельно** для `install`, `activate`, `message`, `sync` - независимые задачи выполняются одновременно
27
- - **Последовательно** для `fetch`, `push` - первый успешный результат прерывает цепочку
27
+ - **Последовательно** для `fetch` первый успешный результат прерывает цепочку; для `push` — вызываются все плагины, показывается уведомление по каждому payload (или одно от библиотеки только когда **все** плагины вернули `undefined`)
28
28
  - **Производительность** - правильный выбор стратегии для каждого типа события
29
29
 
30
30
  ### 🛡️ **Централизованная обработка ошибок**
@@ -59,84 +59,103 @@ pnpm add @budarin/pluggable-serviceworker
59
59
  ```typescript
60
60
  // sw.js
61
61
  import {
62
- initServiceWorker,
62
+ type PluginContext,
63
63
  type ServiceWorkerPlugin,
64
- type SwContext,
64
+ initServiceWorker,
65
65
  } from '@budarin/pluggable-serviceworker';
66
66
 
67
- // Контекст: список ассетов для precache и имя кеша
68
- interface PrecacheAndServeContext extends SwContext {
69
- assets: string[];
67
+ function precacheAndServePlugin(config: {
70
68
  cacheName: string;
69
+ assets: string[];
70
+ }): ServiceWorkerPlugin<PluginContext> {
71
+ const { cacheName, assets } = config;
72
+
73
+ return {
74
+ name: 'precache-and-serve',
75
+
76
+ install: async (_event, logger) => {
77
+ if (import.meta.env.DEV) {
78
+ logger.debug('precache-and-serve: cache assets');
79
+ }
80
+
81
+ const cache = await caches.open(cacheName);
82
+ await cache.addAll(assets);
83
+ },
84
+
85
+ fetch: async (event, logger) => {
86
+ const cache = await caches.open(cacheName);
87
+ const asset = await cache.match(event.request);
88
+
89
+ if (!asset && import.meta.env.DEV) {
90
+ logger.debug(
91
+ `precache-and-serve: asset ${event.request.url} not found in cache!`
92
+ );
93
+ }
94
+
95
+ return asset ?? undefined;
96
+ },
97
+ };
71
98
  }
72
99
 
73
- const precacheAndServePlugin: ServiceWorkerPlugin<PrecacheAndServeContext> = {
74
- name: 'precache-and-serve',
100
+ initServiceWorker([
101
+ precacheAndServePlugin({
102
+ cacheName: 'my-cache-v1',
103
+ assets: ['/', '/styles.css', '/script.js'],
104
+ }),
105
+ ]);
106
+ ```
75
107
 
76
- install: async (_event, context) => {
77
- const cache = await caches.open(context.cacheName);
78
- await cache.addAll(context.assets);
79
- },
108
+ **Важно:**
80
109
 
81
- fetch: async (event, context) => {
82
- const cache = await caches.open(context.cacheName);
83
- return cache.match(event.request) ?? undefined;
84
- },
85
- };
110
+ - для `fetch` плагину не нужно самому вызывать `fetch(event.request)` если все плагины вернули `undefined`, фреймворк сам идёт в сеть.
111
+ - конфиг плагина задаётся по месту вызова фабрики; в `options` только `logger?` и `onError?`.
86
112
 
87
- // TypeScript проверит, что в options есть assets и cacheName
88
- initServiceWorker([precacheAndServePlugin], {
89
- logger: console,
90
- assets: ['/', '/styles.css', '/script.js'],
91
- cacheName: 'my-cache-v1',
92
- });
93
- ```
113
+ ### Фабрика плагинов
94
114
 
95
- **Важно:** для `fetch` плагину не нужно самому вызывать `fetch(event.request)`, если все плагины вернули `undefined` - фреймворк сам выполняет запрос в сеть. Во все обработчики плагинов вторым аргументом передаётся **контекст** — те же данные, что вы передали в `initServiceWorker`.
115
+ **Плагин** это объект с полем `name` и опциональными обработчиками (`install`, `fetch`, `activate` и т.д.). В массив `initServiceWorker(plugins, options)` передаются именно такие объекты.
96
116
 
97
- ## Демо
117
+ **Фабрика плагина** — функция, которая принимает конфиг и возвращает плагин (объект). Например: `precache(config)`, `serveFromCache(config)` или собственная `precacheAndServePlugin(config)` из примера выше. Конфиг задаётся по месту вызова фабрики; в общий `options` попадают только `logger?` и `onError?`.
98
118
 
99
- В папке [demo/](demo/) — приложение **React + Vite** с пресетом **offlineFirst** и типовым сервис-воркером **activateOnSignal**. Запуск из корня: `pnpm install && pnpm build`, затем `cd demo && pnpm install && pnpm run dev`. Подробности и ссылки на публичные песочницы (StackBlitz, CodeSandbox) — в [demo/README.md](demo/README.md).
119
+ ## Демо
100
120
 
101
- [Open in StackBlitz](https://stackblitz.com/github/budarin/pluggable-serviceworker/tree/master/demo) · [Open in CodeSandbox](https://codesandbox.io/s/github/budarin/pluggable-serviceworker/tree/master/demo)
121
+ В папке [demo/](demo/) приложение **React + Vite** с пресетом **offlineFirst** и типовым сервис-воркером **activateOnSignal**. Запуск из корня: `pnpm start`. Подробности — в [demo/README.md](demo/README.md).
102
122
 
103
123
  ## `initServiceWorker(plugins, options)`
104
124
 
105
- `initServiceWorker` — точка входа: она регистрирует обработчики событий Service Worker (`install`, `activate`, `fetch`, …) и прогоняет их через список плагинов.
125
+ `initServiceWorker` — точка входа: регистрирует обработчики событий Service Worker (`install`, `activate`, `fetch`, …) и прогоняет их через список плагинов.
106
126
 
107
- - `plugins`: массив плагинов
108
- - `options`: один общий объект с настройками/данными, который будет доступен плагинам как **контекст**
127
+ - **`plugins`** массив плагинов (объектов). Плагины с конфигом получаются вызовом **фабрик** по месту использования (см. раздел «Фабрика плагинов»).
128
+ - **`options`** только `logger?` и `onError?`. В обработчики плагинов вторым аргументом передаётся **logger** (из `options` или `console`).
109
129
 
110
130
  **Пример:**
111
131
 
112
132
  ```typescript
113
- initServiceWorker([myPlugin], {
114
- logger: console,
115
- // ... тут будут поля контекста, которые требуют плагины ...
116
- });
133
+ initServiceWorker(
134
+ [
135
+ precache({ cacheName: 'v1', assets: ['/'] }),
136
+ serveFromCache({ cacheName: 'v1' }),
137
+ ],
138
+ { logger: serverLogger, onError: handleError }
139
+ );
117
140
  ```
118
141
 
119
- ## ⚙️ Конфигурация и контекст (options)
142
+ ## ⚙️ Опции initServiceWorker (logger, onError)
120
143
 
121
- Функция `initServiceWorker` принимает второй параметр `options` типа `ServiceWorkerInitOptions` (контекст для плагинов + `onError` для библиотеки). В обработчики плагинов вторым аргументом передаётся **контекст** часть этого объекта без `onError` (тип контекста `SwContext` и ваши поля; при типизированных плагинах пересечение требуемых ими полей).
144
+ Второй параметр `options` типа `ServiceWorkerInitOptions`: в нём только `logger?` и `onError?`. В обработчики плагинов передаётся только **logger** (второй аргумент); если `logger` не указан, используется `console`. Поле `onError` нужно только библиотеке, в плагины не передаётся.
145
+
146
+ Тип `PluginContext` в API используется для типизации (содержит `logger?`); «богатого контекста» плагинам не передаётся.
122
147
 
123
148
  ```typescript
124
- interface SwContext {
149
+ interface PluginContext {
125
150
  logger?: Logger; // по умолчанию console
126
- // сюда можно добавлять свои поля: version, assets, cacheName и т.д.
127
151
  }
128
152
 
129
- // В initServiceWorker передаётся ServiceWorkerInitOptions = SwContext + onError:
130
- interface ServiceWorkerInitOptions extends SwContext {
153
+ interface ServiceWorkerInitOptions extends PluginContext {
131
154
  onError?: (error, event, errorType?) => void; // только для библиотеки, в плагины не передаётся
132
155
  }
133
156
  ```
134
157
 
135
- В тип контекста, который видят плагины, входит только `SwContext` и ваши поля; `onError` в этот тип не входит и используется только библиотекой.
136
-
137
- Формируйте объект `options` в своём сервис-воркере (контекст для плагинов + при необходимости `onError`) и передавайте его в `initServiceWorker`. В плагины передаётся тот же объект как контекст — плагины получают доступ к полям контекста, а `onError` остаётся внутренним делом библиотеки.
138
-
139
- ### Поля конфигурации
158
+ ### Поля options
140
159
 
141
160
  #### `logger?: Logger` (опциональное)
142
161
 
@@ -155,17 +174,15 @@ interface Logger {
155
174
  **Пример:**
156
175
 
157
176
  ```typescript
158
- const logger = console; // Использование стандартного console
159
-
160
177
  const options = {
161
- logger,
178
+ logger: customLogger, // Использование кастомного логгера
162
179
  // или
163
180
  logger: {
164
- trace: (...data) => customLog('TRACE', ...data),
165
- debug: (...data) => customLog('DEBUG', ...data),
166
- info: (...data) => customLog('INFO', ...data),
167
- warn: (...data) => customLog('WARN', ...data),
168
- error: (...data) => customLog('ERROR', ...data),
181
+ trace: (...data) => customLogger('TRACE', ...data),
182
+ debug: (...data) => customLogger('DEBUG', ...data),
183
+ info: (...data) => customLogger('INFO', ...data),
184
+ warn: (...data) => customLogger('WARN', ...data),
185
+ error: (...data) => customLogger('ERROR', ...data),
169
186
  },
170
187
  };
171
188
  ```
@@ -186,7 +203,7 @@ const options = {
186
203
 
187
204
  ```typescript
188
205
  // Без onError - ошибки будут проигнорированы
189
- initServiceWorker([cachePlugin], {});
206
+ initServiceWorker([cachePlugin]);
190
207
 
191
208
  // С onError - ошибки будут обработаны
192
209
  initServiceWorker([cachePlugin], {
@@ -270,74 +287,63 @@ initServiceWorker(
270
287
 
271
288
  ## 🔌 Интерфейс плагина
272
289
 
273
- Каждый плагин реализует интерфейс `ServiceWorkerPlugin<C>`, где `C extends SwContext` тип контекста, который плагин ожидает. Во все обработчики вторым аргументом передаётся контекст (те же данные, что в `options` при инициализации, без `onError`).
290
+ Плагин объект, реализующий интерфейс `ServiceWorkerPlugin`. Во все обработчики вторым аргументом передаётся **logger** (всегда определён: из `options` или `console`). Специфичный для плагина конфиг задаётся при вызове **фабрики** плагина; в `options` только `logger?` и `onError?`. Параметр типа `_C` (например `PluginContext`) используется для типизации; по умолчанию контекст содержит только `logger`.
274
291
 
275
292
  ```typescript
276
- interface ServiceWorkerPlugin<C extends SwContext = SwContext> {
293
+ interface ServiceWorkerPlugin<_C extends PluginContext = PluginContext> {
277
294
  name: string;
295
+
278
296
  order?: number;
279
297
 
280
- install?: (event: ExtendableEvent, context?: C) => Promise<void> | void;
281
- activate?: (event: ExtendableEvent, context?: C) => Promise<void> | void;
298
+ install?: (event: ExtendableEvent, logger: Logger) => Promise<void> | void;
299
+
300
+ activate?: (event: ExtendableEvent, logger: Logger) => Promise<void> | void;
301
+
282
302
  fetch?: (
283
303
  event: FetchEvent,
284
- context?: C
304
+ logger: Logger
285
305
  ) => Promise<Response | undefined> | Response | undefined;
286
- message?: (event: SwMessageEvent, context?: C) => void;
287
- sync?: (event: SyncEvent, context?: C) => Promise<void> | void;
306
+
307
+ message?: (event: SwMessageEvent, logger: Logger) => void;
308
+
309
+ sync?: (event: SyncEvent, logger: Logger) => Promise<void> | void;
310
+
288
311
  push?: (
289
312
  event: PushEvent,
290
- context?: C
313
+ logger: Logger
291
314
  ) =>
292
315
  | Promise<PushNotificationPayload | void>
293
316
  | PushNotificationPayload
294
317
  | void;
318
+
295
319
  periodicsync?: (
296
320
  event: PeriodicSyncEvent,
297
- context?: C
321
+ logger: Logger
298
322
  ) => Promise<void> | void;
299
323
  }
300
324
  ```
301
325
 
302
- Плагин может объявить требуемый контекст через дженерик: `ServiceWorkerPlugin<SwContext & { assets: string[]; cacheName: string }>`. Тогда TypeScript потребует передать в `initServiceWorker` объект `options` с полями `assets` и `cacheName` (при вызове с литералом массива плагинов тип `options` выводится автоматически).
303
-
304
326
  ### 📝 Описание методов
305
327
 
306
- | Метод | Событие | Возвращает | Описание |
307
- | -------------- | -------------- | --------------------------------- | -------------------------------------------------------------- |
308
- | `install` | `install` | `void` | Инициализация плагина при установке SW |
309
- | `activate` | `activate` | `void` | Активация плагина при обновлении SW |
310
- | `fetch` | `fetch` | `Response \| undefined` | Обработка сетевых запросов |
311
- | `message` | `message` | `void` | Обработка сообщений от основного потока |
312
- | `sync` | `sync` | `void` | Синхронизация данных в фоне |
313
- | `push` | `push` | `PushNotificationPayload \| void` | Данные для уведомления; библиотека вызывает `showNotification` |
314
- | `periodicsync` | `periodicsync` | `void` | Периодические фоновые задачи |
328
+ | Метод | Событие | Возвращает | Описание |
329
+ | -------------- | -------------- | ----------------------------------------------- | ------------------------------------------- |
330
+ | `install` | `install` | `void` | Инициализация плагина при установке SW |
331
+ | `activate` | `activate` | `void` | Активация плагина при обновлении SW |
332
+ | `fetch` | `fetch` | `Response \| undefined` | Обработка сетевых запросов |
333
+ | `message` | `message` | `void` | Обработка сообщений от основного потока |
334
+ | `sync` | `sync` | `void` | Синхронизация данных в фоне |
335
+ | `push` | `push` | `PushNotificationPayload \| false \| undefined` | Обработка и отображение сетевой нотификации |
336
+ | `periodicsync` | `periodicsync` | `void` | Периодические фоновые задачи |
315
337
 
316
338
  ### 🎯 Особенности обработчиков
317
339
 
318
- - Во все методы вторым аргументом передаётся **контекст** (данные из объекта, переданного в `initServiceWorker`, без `onError`). Параметр можно не использовать, если плагину контекст не нужен.
340
+ - Во все методы вторым аргументом передаётся **logger** (всегда определён: из `options` или `console`).
319
341
  - **`fetch`**: Возвращает `Response` для завершения цепочки или `undefined` для передачи следующему плагину. Если все плагины вернули `undefined`, фреймворк вызывает `fetch(event.request)`.
320
- - **`push`**: Как и fetch — возвращает `PushNotificationPayload` (объект для [Notification API](https://developer.mozilla.org/en-US/docs/Web/API/Notification)) или `undefined`. Тип экспортируется из пакета. Первый плагин, вернувший объект с `title`, «выигрывает»: библиотека вызывает `self.registration.showNotification(title, options)`. Если все вернули `undefined`, уведомление не показывается.
342
+ - **`push`**: Возвращает `PushNotificationPayload` (объект для [Notification API](https://developer.mozilla.org/en-US/docs/Web/API/Notification)), `false` (не отображать уведомление) или `undefined` (решение об отображении отдаётся библиотеке). Вызываются все плагины с `push`. Для каждого возврата типа `PushNotificationPayload` вызывается `showNotification`. Уведомление не показывается, если все вернули `false` или смесь `undefined` и `false` без payload. Библиотека отображает одно уведомление **только когда все** плагины вернули `undefined` (и в данных есть что показывать).
321
343
  - **Остальные обработчики** (`install`, `activate`, `message`, `sync`, `periodicsync`): возвращаемое значение не используется; фреймворк вызывает метод каждого плагина по очереди, цепочка не прерывается.
322
344
  - **Все обработчики опциональны** — реализуйте только нужные события.
323
345
 
324
- ### 🔄 Обновление Service Worker (skipWaiting / clients.claim)
325
-
326
- Библиотека **не вызывает** `skipWaiting()` и `clients.claim()` — это поведение задаётся индивидуально в каждом проекте и оставлено на усмотрение плагинов. При необходимости вызывайте их в своих обработчиках `install` и `activate` (в библиотеке реализованы данные примитивы - смотри ниже):
327
-
328
- ```typescript
329
- const updatePlugin = {
330
- name: 'update-plugin',
331
- install: (event) => {
332
- self.skipWaiting();
333
- },
334
- activate: (event) => {
335
- event.waitUntil(self.clients.claim());
336
- },
337
- };
338
- ```
339
-
340
- ## 🎯 Порядок выполнения
346
+ ## 🎯 Порядок выполнения плагинов
341
347
 
342
348
  Плагины выполняются в следующем порядке:
343
349
 
@@ -349,16 +355,16 @@ const updatePlugin = {
349
355
  ```typescript
350
356
  const plugins = [
351
357
  { name: 'first' }, // без order - выполняется первым
352
- { name: 'fourth', order: 2 },
358
+ { name: 'fifth', order: 4 },
359
+ { name: 'fourth', order: 3 },
353
360
  { name: 'second' }, // без order - выполняется вторым
354
- { name: 'third', order: 1 },
355
- { name: 'fifth' }, // без order - выполняется третьим
361
+ { name: 'third', order: 2 },
356
362
  ];
357
363
 
358
- // Порядок выполнения: first → second → fifththirdfourth
364
+ // Порядок выполнения: first → second → thirdfourthfifth
359
365
  ```
360
366
 
361
- **Преимущества новой системы:**
367
+ **Преимущества системы:**
362
368
 
363
369
  - 🎯 **Предсказуемость** - плагины без `order` всегда выполняются первыми
364
370
  - 🔧 **Простота** - не нужно знать, какие номера уже заняты
@@ -375,21 +381,24 @@ const plugins = [
375
381
  Все обработчики выполняются **одновременно** с помощью `Promise.all()`:
376
382
 
377
383
  ```typescript
378
- // Все плагины инициализируются параллельно
379
- const installPlugin1 = {
380
- name: 'cache-assets',
381
- install: async () => {
382
- /* кеширование ресурсов приложения*/
383
- },
384
- };
385
- const installPlugin2 = {
386
- name: 'cache-ext',
387
- install: async () => {
388
- /* кэширование вспомогательных ресурсов */
389
- },
390
- };
384
+ import {
385
+ precache,
386
+ skipWaiting,
387
+ precacheMissing,
388
+ } from '@budarin/pluggable-serviceworker/plugins';
391
389
 
392
- // Оба install обработчика выполнятся одновременно
390
+ import { customLogger } from '../customLogger';
391
+ import { initServiceWorker } from '@budarin/pluggable-serviceworker';
392
+
393
+ // Все install-обработчики (precache, precacheMissing, skipWaiting) выполнятся параллельно
394
+ initServiceWorker(
395
+ [
396
+ precache({ cacheName: 'app-v1', assets: ['/', '/main.js'] }),
397
+ precacheMissing({ cacheName: 'ext-v1', assets: ['/worker.js'] }),
398
+ skipWaiting,
399
+ ],
400
+ { logger: customLogger }
401
+ );
393
402
  ```
394
403
 
395
404
  **Почему параллельно:**
@@ -403,39 +412,62 @@ const installPlugin2 = {
403
412
 
404
413
  **События:** `fetch`, `push`
405
414
 
406
- Обработчики выполняются **по очереди** до первого успешного результата:
415
+ Обработчики выполняются **по очереди**:
416
+
417
+ #### Fetch — с прерыванием цепочки
407
418
 
408
- #### Fetch - с прерыванием цепочки
419
+ Обработчики `fetch` вызываются **по очереди**. Плагин может вернуть `Response` — тогда цепочка прерывается и этот ответ уходит клиенту. Либо вернуть `undefined` — тогда запрос передаётся следующему плагину. Если **все** плагины вернули `undefined`, фреймворк сам выполняет `fetch(event.request)`.
420
+
421
+ Пример фабрики, которая прерывает цепочку при неавторизованном доступе к защищённым путям:
409
422
 
410
423
  ```typescript
411
- const authPlugin = {
412
- name: 'auth',
413
- // Без order - выполняется первым
414
- fetch: async (event) => {
415
- if (needsAuth(event.request)) {
416
- return new Response('Unauthorized', { status: 401 }); // Прерывает цепочку
417
- }
418
- return undefined; // Передает следующему плагину
419
- },
420
- };
424
+ import type {
425
+ PluginContext,
426
+ ServiceWorkerPlugin,
427
+ } from '@budarin/pluggable-serviceworker';
428
+
429
+ function authPlugin(config: {
430
+ protectedPaths: string[];
431
+ }): ServiceWorkerPlugin<PluginContext> {
432
+ const { protectedPaths } = config;
433
+
434
+ return {
435
+ name: 'auth',
436
+
437
+ fetch: async (event, logger) => {
438
+ const path = new URL(event.request.url).pathname;
439
+
440
+ if (protectedPaths.some((p) => path.startsWith(p))) {
441
+ if (needsAuth(event.request)) {
442
+ logger.warn('auth: unauthorized', event.request.url);
443
+
444
+ return new Response('Unauthorized', { status: 401 }); // Прерывает цепочку
445
+ }
446
+ }
447
+ return undefined; // Передаёт следующему плагину
448
+ },
449
+ };
450
+ }
451
+
452
+ // Использование: authPlugin({ protectedPaths: ['/api/'] })
421
453
  ```
422
454
 
423
455
  **Почему последовательно:**
424
456
 
425
- - **fetch**: Нужен только один ответ, первый успешный прерывает цепочку. Если никто не вернул ответ — выполняется `fetch(event.request)`
426
- - **push**: Плагин может вернуть данные для уведомления типа `PushNotificationPayload` | `undefined`. Библиотека один раз вызывает `showNotification` по первому вернувшемуся payload; цепочка прерывается одно уведомление, без конфликтов.
457
+ - **fetch**: Нужен только один ответ на текущий запрос браузера, первый успешный прерывает цепочку. Если никто не вернул ответ — выполняется `fetch(event.request)`
458
+ - **push**: Плагин может вернуть `PushNotificationPayload`, `false` (не показывать) или `undefined` (решение отдаётся библиотеке). Библиотека вызывает `showNotification` для каждого payload. Не показываем, если все вернули `false` или смесь без payload. Библиотека показывает нотификацию и в случае когда **все** плагины вернули `undefined`.
427
459
 
428
460
  ### 📋 Сводная таблица
429
461
 
430
- | Событие | Выполнение | Прерывание | Причина |
431
- | -------------- | --------------- | ---------- | ------------------------------------------------------ |
432
- | `install` | Параллельно | Нет | Независимая инициализация |
433
- | `activate` | Параллельно | Нет | Независимая активация |
434
- | `fetch` | Последовательно | Да | Нужен один ответ |
435
- | `message` | Параллельно | Нет | Все получают сообщение |
436
- | `sync` | Параллельно | Нет | Независимые задачи |
437
- | `periodicsync` | Параллельно | Нет | Независимые периодические задачи |
438
- | `push` | Последовательно | Да | Один показ уведомления (библиотека по первому payload) |
462
+ | Событие | Выполнение | Прерывание | Причина |
463
+ | -------------- | ----------------- | ---------- | -------------------------------------- |
464
+ | `install` | `Параллельно` | `Нет` | Независимая инициализация |
465
+ | `activate` | `Параллельно` | `Нет` | Независимая активация |
466
+ | `fetch` | `Последовательно` | `Да` | Нужен один ответ |
467
+ | `message` | `Параллельно` | `Нет` | Независимые обработчики сообщений |
468
+ | `sync` | `Параллельно` | `Нет` | Независимые задачи |
469
+ | `periodicsync` | `Параллельно` | `Нет` | Независимые периодические задачи |
470
+ | `push` | `Последовательно` | `Нет` | Отображение всех необходимых сообщений |
439
471
 
440
472
  ## Примитивы, пресеты и типовые сервис-воркеры
441
473
 
@@ -443,59 +475,215 @@ const authPlugin = {
443
475
 
444
476
  Один примитив — одна операция. Импорт: `@budarin/pluggable-serviceworker/plugins`.
445
477
 
446
- | Название | Событие | Описание |
447
- | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
448
- | **precache** | install | Кеширует список `context.assets` в кеш `context.cacheName`. |
449
- | **skipWaiting** | install | Вызывает `skipWaiting()`. |
450
- | **claim** | activate | Вызывает `clients.claim()`. |
451
- | **claimOnMessage** | message | При сообщении с `event.data.type === context.claimMessageType` (по умолчанию `'SW_ACTIVATE'`) вызывает `skipWaiting()`. `clients.claim()` вызывается плагином **claim** в activate. |
452
- | **serveFromCache** | fetch | Отдаёт из кеша; при промахе undefined. |
453
- | **restoreAssetToCache** | fetch | Для URL из `context.assets`: сначала из кеша; если в кеше нет — запрос с сервера, в кеш, ответ браузеру. |
454
- | **cacheFirst** | fetch | Кеш → при промахе сеть, ответ в кеш. |
455
- | **networkFirst** | fetch | Сеть → при ошибке/офлайне из кеша. |
456
- | **staleWhileRevalidate** | fetch | Отдаёт из кеша, в фоне обновляет кеш из сети. |
478
+ Примитивы с конфигом — **фабрики плагинов** (см. раздел «Фабрика плагинов»): конфиг передаётся при вызове по месту использования; в `options` в `initServiceWorker` попадают только `logger?` и `onError?`. Примитивы без конфига (`skipWaiting`, `claim`, …) — готовые объекты плагинов.
479
+
480
+ | Название | Событие | Описание |
481
+ | ------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------- |
482
+ | `precache*(config)` | `install` | Кеширует список ресурсов из `config.assets` в кеш `config.cacheName`. |
483
+ | `precacheAndNotify(config)` | `install` | Как **precache**, затем отправляет активным клиентам сообщение `{ type: config.messageType }` (по умолчанию `SW_INSTALLED`). |
484
+ | `precacheMissing(config)` | `install` | Добавляет в кеш только те ресурсы из `config.assets`, которых ещё нет в кеше. |
485
+ | `pruneStaleCache(config)` | `activate` | Удаляет из кеша записи, чей URL не входит в `config.assets`. |
486
+ | `skipWaiting` | `install` | Вызывает `skipWaiting()`. |
487
+ | `claim` | `activate` | Вызывает `clients.claim()`. |
488
+ | `reloadClients` | `activate` | Перезагружает все окна-клиенты через `client.navigate(client.url)`. |
489
+ | `claimAndReloadClients` | `activate` | Композиция **claim** + **reloadClients**: сначала claim, затем перезагрузка (порядок гарантирован — один плагин). |
490
+ | `skipWaitingOnMessage(config?)` | `message` | При сообщении с `event.data.type === 'SW_MSG_SKIP_WAITING'` вызывает `skipWaiting()`. |
491
+ | `serveFromCache(config)` | `fetch` | Отдаёт ресурс из кеша `config.cacheName`; при отсутствии его в кэше — undefined. |
492
+ | `restoreAssetToCache(config)` | `fetch` | Для URL из `config.assets`: отдам ресурс из кеша или запрашиваем по сети, затем в кладем кго в кеш. Иначе — undefined. |
493
+ | `cacheFirst(config)` | `fetch` | Отдаем ресурс из кэша `config.cacheName`: при отсутствии его в кэше — делаем запрос на сервер и затем кладем ответ в кэш. |
494
+ | `networkFirst(config)` | `fetch` | Делаем запрос на сервер, при успехе — кладем его в кэш. При ошибке — отдаем из кэша. Иначе - `undefined`. |
495
+ | `staleWhileRevalidate(config)` | `fetch` | Отдаёт из кэша, в фоне обновляет кэш. |
496
+
497
+ #### Композиция примитивов
498
+
499
+ Обработчики одного типа (`install`, `activate` и т.д.) у разных плагинов выполняются **параллельно**. Если нужна строгая последовательность (например «сначала claim, потом перезагрузка клиентов»), соберите один плагин, который по очереди вызывает логику примитивов — для гарантии порядка.
500
+
501
+ **Пример: claimAndReloadClients как композиция двух примитивов**
502
+
503
+ Плагин **claimAndReloadClients** вызывает существующие примитивы **claim** и **reloadClients** по очереди:
504
+
505
+ ```typescript
506
+ import { claim } from '@budarin/pluggable-serviceworker/plugins';
507
+ import { reloadClients } from '@budarin/pluggable-serviceworker/plugins';
508
+
509
+ const claimPlugin = claim();
510
+ const reloadPlugin = reloadClients();
511
+
512
+ activate: (event, logger) =>
513
+ Promise.resolve(claimPlugin.activate(event, logger)).then(() =>
514
+ reloadPlugin.activate(event, logger)
515
+ ),
516
+ ```
517
+
518
+ **Пример: кастомный кэш и логика по URL**
519
+
520
+ Фабрика `postsSwrPlugin(config)` возвращает плагин, который применяет `stale-while-revalidate`(SWR) только к запросам, подходящим под `pathPattern`.
521
+
522
+ ```typescript
523
+ // postsSwrPlugin.ts
524
+ import type { Plugin } from '@budarin/pluggable-serviceworker';
457
525
 
458
- Контекст для кеширующих примитивов: `OfflineFirstContext` (assets, cacheName, опционально claimMessageType). Импортируйте тип из основного пакета.
526
+ import {
527
+ precache,
528
+ serveFromCache,
529
+ } from '@budarin/pluggable-serviceworker/plugins';
530
+ import { initServiceWorker } from '@budarin/pluggable-serviceworker';
531
+
532
+ function postsSwrPlugin(config: {
533
+ cacheName: string;
534
+ pathPattern?: RegExp;
535
+ }): Plugin {
536
+ const { cacheName, pathPattern = /\/api\/posts(\/|$)/ } = config;
537
+
538
+ return {
539
+ name: 'postsSwr',
540
+ order: 0,
541
+
542
+ fetch: async (event) => {
543
+ if (!pathPattern.test(new URL(event.request.url).pathname)) {
544
+ return undefined;
545
+ }
546
+
547
+ const cache = await caches.open(cacheName);
548
+ const cached = await cache.match(event.request);
549
+ const revalidate = fetch(event.request).then(async (response) => {
550
+ if (response.ok) {
551
+ await cache.put(event.request, response.clone());
552
+ }
553
+
554
+ return response;
555
+ });
556
+
557
+ if (cached) {
558
+ void revalidate;
559
+ return cached;
560
+ }
561
+
562
+ return revalidate;
563
+ },
564
+ };
565
+ }
566
+ ```
567
+
568
+ ```typescript
569
+ // sw.ts
570
+ const staticCache = 'static-v1';
571
+ const assets = ['/', '/main.js'];
572
+
573
+ initServiceWorker(
574
+ [
575
+ precache({ cacheName: staticCache, assets }),
576
+ serveFromCache({ cacheName: staticCache }),
577
+ postsSwrPlugin({ cacheName: 'posts' }),
578
+ ],
579
+ { logger: console }
580
+ );
581
+ ```
459
582
 
460
583
  ### Пресеты
461
584
 
462
585
  Комбинации примитивов (стратегии кеширования). Импорт: `@budarin/pluggable-serviceworker/presets`.
463
586
 
464
- | Название | Состав | Назначение |
465
- | ---------------- | ------------------------- | ------------------------------------ |
466
- | **offlineFirst** | precache + serveFromCache | Статика из кеша, при промахесеть. |
587
+ | Название | Состав | Назначение |
588
+ | ---------------------- | ------------------------------------------- | ------------------------------------------------------------------------- |
589
+ | `offlineFirst(config)` | `precache(config) + serveFromCache(config)` | Статика из кеша, при отсутствии ресурса в кэше делаем запрос к серверу. |
467
590
 
591
+ Конфиг пресета: `OfflineFirstConfig` (cacheName, assets). Импорт из `@budarin/pluggable-serviceworker/presets`.
468
592
  Стратегии **networkFirst**, **staleWhileRevalidate** и др. доступны как примитивы — собирайте свой кастомный сервис-воркер из примитивов и пресетов.
469
593
 
470
594
  ### Типовые сервис-воркеры (из коробки)
471
595
 
472
596
  Готовые точки входа по **моменту активации** (все с кешированием offline-first). Импорт: `@budarin/pluggable-serviceworker/sw`.
473
597
 
474
- | Название | Описание |
475
- | ------------------------------------ | --------------------------------------------------------------------------------------------------- |
476
- | **activateOnNextVisitServiceWorker** | Кеширующий SW, активируется при следующем визите страницы. |
477
- | **activateImmediatelyServiceWorker** | Кеширующий SW, активируется сразу (skipWaiting + claim). |
478
- | **activateOnSignalServiceWorker** | Кеширующий SW, активируется по сигналу со страницы (сообщение с типом из options.claimMessageType). |
598
+ | Название | Описание |
599
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
600
+ | `activateOnNextVisitServiceWorker` | Кеширующий SW, активируется и обновляется при следующем визите на страницу (перезагрузке) после загрузки нового сервисворкера. |
601
+ | `activateImmediatelyServiceWorker` | Кеширующий SW, активируется и вступает в действие сразу при 1-й загрузке и при обновлении. |
602
+ | `activateOnSignalServiceWorker` | Кеширующий SW, устанавливается сразу, но активируется при обновлении только по сигналу со страницы (сообщение `SW_MSG_SKIP_WAITING`). |
479
603
 
480
604
  Пример использования типового SW:
481
605
 
482
606
  ```typescript
483
607
  // sw.js — точка входа вашего сервис-воркера
608
+ import { customLogger } from './customLogger';
484
609
  import { activateOnNextVisitServiceWorker } from '@budarin/pluggable-serviceworker/sw';
485
610
 
486
611
  activateOnNextVisitServiceWorker({
487
612
  assets: ['/', '/styles.css', '/script.js'],
488
613
  cacheName: 'my-cache-v1',
489
- logger: console,
490
- onError: (err, event, type) => console.error(type, err),
614
+ logger: customLogger,
615
+ onError: (err, event, type) => customLogger.error(type, err),
491
616
  });
492
617
  ```
493
618
 
494
619
  На странице регистрируйте этот файл: `navigator.serviceWorker.register('/sw.js')` (или путь, по которому сборка отдаёт ваш sw.js).
495
620
 
621
+ ### Публикуемые утилиты
622
+
623
+ Утилиты, доступные для использования в своих плагинах. Импорт: `@budarin/pluggable-serviceworker/utils`.
624
+
625
+ | Название | Описание |
626
+ | ---------------------------- | ------------------------------------------------------------------------ |
627
+ | `normalizeUrl(url)` | Нормализует URL (относительный → абсолютный по origin SW) для сравнения. |
628
+ | `notifyClients(messageType)` | Отправляет сообщение `{ type: messageType }` всем окнам-клиентам (SW). |
629
+
630
+ ## Разработка пакета плагина
631
+
632
+ Типы для описания плагина экспортируются из этого пакета. Отдельный пакет с плагином не публикует свои типы — он объявляет зависимость от `@budarin/pluggable-serviceworker` и импортирует типы оттуда.
633
+
634
+ **1. Зависимости в пакете плагина**
635
+
636
+ В `package.json` своего пакета добавьте:
637
+
638
+ ```json
639
+ {
640
+ "peerDependencies": {
641
+ "@budarin/pluggable-serviceworker": "^1.0.0"
642
+ },
643
+ "devDependencies": {
644
+ "@budarin/pluggable-serviceworker": "^1.5.0"
645
+ }
646
+ }
647
+ ```
648
+
649
+ `peerDependencies` — чтобы плагин работал с той версией библиотеки, которую установил пользователь; в `devDependencies` — для сборки и типов.
650
+
651
+ **2. Импорт типов в коде плагина**
652
+
653
+ Импортируйте тип **`Plugin`** (алиас для `ServiceWorkerPlugin<PluginContext>`); при необходимости — `Logger`, `SwMessageEvent`, `PushNotificationPayload` и др.
654
+
655
+ ```typescript
656
+ import type { Plugin } from '@budarin/pluggable-serviceworker';
657
+
658
+ export interface MyPluginConfig {
659
+ cacheName: string;
660
+ }
661
+
662
+ export function myPlugin(config: MyPluginConfig): Plugin {
663
+ const { cacheName } = config;
664
+
665
+ return {
666
+ name: 'my-plugin',
667
+
668
+ install: async (_event, logger) => {
669
+ logger.info('my-plugin: install');
670
+ const cache = await caches.open(cacheName);
671
+ await cache.add('/offline.html');
672
+ },
673
+
674
+ fetch: async (event, _logger) => {
675
+ const cached = await caches.match(event.request);
676
+ return cached ?? undefined;
677
+ },
678
+ };
679
+ }
680
+ ```
681
+
682
+ Пользователь подключает плагин так: `initServiceWorker([..., myPlugin({ cacheName: 'my-v1' })], options)`.
683
+
496
684
  ## Режим разработки
497
685
 
498
- Если нужно, чтобы в режиме разработки ни один из плагинов ничего не кэшировал и не пытался что-либо отдавать из кэша - в плагине и сервисворкере можно использовть `import.meta.env.DEV` для Vite для условного использования кэширования.
686
+ В режиме разработки - используйте `import.meta.env.DEV` для Vite для условного использования кода.
499
687
 
500
688
  ## 📄 Лицензия
501
689