@budarin/pluggable-serviceworker 1.1.1 → 1.2.0

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 (74) hide show
  1. package/README.md +502 -421
  2. package/dist/index.d.ts +33 -16
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +46 -29
  5. package/dist/index.js.map +1 -1
  6. package/dist/plugins/cacheFirst.d.ts +3 -0
  7. package/dist/plugins/cacheFirst.d.ts.map +1 -0
  8. package/dist/plugins/cacheFirst.js +20 -0
  9. package/dist/plugins/cacheFirst.js.map +1 -0
  10. package/dist/plugins/claim.d.ts +3 -0
  11. package/dist/plugins/claim.d.ts.map +1 -0
  12. package/dist/plugins/claim.js +7 -0
  13. package/dist/plugins/claim.js.map +1 -0
  14. package/dist/plugins/claimOnMessage.d.ts +3 -0
  15. package/dist/plugins/claimOnMessage.d.ts.map +1 -0
  16. package/dist/plugins/claimOnMessage.js +11 -0
  17. package/dist/plugins/claimOnMessage.js.map +1 -0
  18. package/dist/plugins/index.d.ts +10 -0
  19. package/dist/plugins/index.d.ts.map +1 -0
  20. package/dist/plugins/index.js +10 -0
  21. package/dist/plugins/index.js.map +1 -0
  22. package/dist/plugins/networkFirst.d.ts +3 -0
  23. package/dist/plugins/networkFirst.d.ts.map +1 -0
  24. package/dist/plugins/networkFirst.js +17 -0
  25. package/dist/plugins/networkFirst.js.map +1 -0
  26. package/dist/plugins/networkOnly.d.ts +3 -0
  27. package/dist/plugins/networkOnly.d.ts.map +1 -0
  28. package/dist/plugins/networkOnly.js +5 -0
  29. package/dist/plugins/networkOnly.js.map +1 -0
  30. package/dist/plugins/precache.d.ts +3 -0
  31. package/dist/plugins/precache.d.ts.map +1 -0
  32. package/dist/plugins/precache.js +8 -0
  33. package/dist/plugins/precache.js.map +1 -0
  34. package/dist/plugins/restoreAssetToCache.d.ts +3 -0
  35. package/dist/plugins/restoreAssetToCache.d.ts.map +1 -0
  36. package/dist/plugins/restoreAssetToCache.js +42 -0
  37. package/dist/plugins/restoreAssetToCache.js.map +1 -0
  38. package/dist/plugins/serveFromCache.d.ts +3 -0
  39. package/dist/plugins/serveFromCache.d.ts.map +1 -0
  40. package/dist/plugins/serveFromCache.js +11 -0
  41. package/dist/plugins/serveFromCache.js.map +1 -0
  42. package/dist/plugins/skipWaiting.d.ts +3 -0
  43. package/dist/plugins/skipWaiting.d.ts.map +1 -0
  44. package/dist/plugins/skipWaiting.js +7 -0
  45. package/dist/plugins/skipWaiting.js.map +1 -0
  46. package/dist/plugins/staleWhileRevalidate.d.ts +3 -0
  47. package/dist/plugins/staleWhileRevalidate.d.ts.map +1 -0
  48. package/dist/plugins/staleWhileRevalidate.js +19 -0
  49. package/dist/plugins/staleWhileRevalidate.js.map +1 -0
  50. package/dist/presets/index.d.ts +2 -0
  51. package/dist/presets/index.d.ts.map +1 -0
  52. package/dist/presets/index.js +2 -0
  53. package/dist/presets/index.js.map +1 -0
  54. package/dist/presets/offlineFirst.d.ts +3 -0
  55. package/dist/presets/offlineFirst.d.ts.map +1 -0
  56. package/dist/presets/offlineFirst.js +7 -0
  57. package/dist/presets/offlineFirst.js.map +1 -0
  58. package/dist/sw/activateImmediately.d.ts +3 -0
  59. package/dist/sw/activateImmediately.d.ts.map +1 -0
  60. package/dist/sw/activateImmediately.js +8 -0
  61. package/dist/sw/activateImmediately.js.map +1 -0
  62. package/dist/sw/activateOnNextVisit.d.ts +3 -0
  63. package/dist/sw/activateOnNextVisit.d.ts.map +1 -0
  64. package/dist/sw/activateOnNextVisit.js +6 -0
  65. package/dist/sw/activateOnNextVisit.js.map +1 -0
  66. package/dist/sw/activateOnSignal.d.ts +3 -0
  67. package/dist/sw/activateOnSignal.d.ts.map +1 -0
  68. package/dist/sw/activateOnSignal.js +8 -0
  69. package/dist/sw/activateOnSignal.js.map +1 -0
  70. package/dist/sw/index.d.ts +4 -0
  71. package/dist/sw/index.d.ts.map +1 -0
  72. package/dist/sw/index.js +4 -0
  73. package/dist/sw/index.js.map +1 -0
  74. package/package.json +7 -4
package/README.md CHANGED
@@ -1,421 +1,502 @@
1
- # @budarin/pluggable-serviceworker
2
-
3
- 🔌 Расширяемый через плагины Service Worker
4
-
5
- Библиотека для создания модульных и расширяемых Service Worker'ов с помощью системы плагинов.
6
-
7
- ## 🚀 Почему этот пакет облегчает разработку?
8
-
9
- Разработка Service Worker'ов традиционно сложна из-за необходимости вручную управлять множественными обработчиками событий, обработкой ошибок и порядком выполнения. Этот пакет решает эти проблемы:
10
-
11
- ### 🔌 **Модульная архитектура**
12
-
13
- - **Плагинная система** позволяет разбивать функциональность на независимые модули
14
- - Каждый плагин отвечает за свою задачу (кеширование, аутентификация, уведомления)
15
- - Легко добавлять/удалять функциональность без изменения основного кода
16
- - Не нужно думать об инфраструктурном коде в обработчиках событий - пишите простой код не думая о сложностях кода самого сервисворкера
17
-
18
- ### 🎯 **Управление порядком выполнения**
19
-
20
- - **Предсказуемый порядок** - плагины без `order` выполняются первыми, затем по возрастанию `order`
21
- - **Гибкость** - можно контролировать последовательность инициализации
22
- - **Масштабируемость** - легко добавлять новые плагины в нужном месте
23
-
24
- ### ⚡ **Оптимизированная логика выполнения**
25
-
26
- - **Параллельно** для `install`, `activate`, `message`, `sync` - независимые задачи выполняются одновременно
27
- - **Последовательно** для `fetch`, `push` - первый успешный результат прерывает цепочку
28
- - **Производительность** - правильный выбор стратегии для каждого типа события
29
-
30
- ### 🛡️ **Централизованная обработка ошибок**
31
-
32
- - **Единый обработчик** для всех типов ошибок
33
- - **Типизированные ошибки** - знаешь, что именно сломалось
34
- - **Изоляция** - ошибка в одном плагине не ломает остальные
35
- - **Автоматическая обработка** глобальных событий ошибок
36
-
37
- ### 📝 **Удобное логирование**
38
-
39
- - **Настраиваемый логгер** с разными уровнями
40
- - **Контекстная информация** в логах
41
- - **Отладка** становится намного проще
42
-
43
- ## 📦 Установка
44
-
45
- ```bash
46
- npm install @budarin/pluggable-serviceworker
47
- ```
48
-
49
- или
50
-
51
- ```bash
52
- pnpm add @budarin/pluggable-serviceworker
53
- ```
54
-
55
- ## 🚀 Быстрый старт
56
-
57
- ### Базовое использование
58
-
59
- ```typescript
60
- // sw.js
61
- import { initServiceWorker } from '@budarin/pluggable-serviceworker';
62
-
63
- // Простой плагин для кеширования
64
- const cachePlugin = {
65
- name: 'cache-plugin',
66
-
67
- install: async (event) => {
68
- const cache = await caches.open('my-cache-v1');
69
- await cache.addAll(['/', '/styles.css', '/script.js']);
70
- },
71
-
72
- fetch: async (event) => {
73
- const cachedResponse = await caches.match(event.request);
74
- return cachedResponse; // если результат undefined → фреймворк сам вызовет fetch(event.request)
75
- },
76
- };
77
-
78
- // Инициализация Service Worker с плагинами
79
- initServiceWorker([cachePlugin], { logger: console });
80
- ```
81
-
82
- **Важно:** для `fetch` плагину не нужно самому вызывать `fetch(event.request)`, если все плагины вернули `undefined` - фреймворк сам выполняет запрос в сеть.
83
-
84
- ## ⚙️ Конфигурация
85
-
86
- Функция `initServiceWorker` принимает второй параметр `config` типа `ServiceWorkerConfig`:
87
-
88
- ```typescript
89
- interface ServiceWorkerConfig {
90
- logger?: Logger; // Опционально, по умолчанию console
91
- onError?: (
92
- error: Error | any,
93
- event: Event,
94
- errorType?: ServiceWorkerErrorType
95
- ) => void;
96
- }
97
- ```
98
-
99
- ### Поля конфигурации
100
-
101
- #### `logger?: Logger` (опциональное)
102
-
103
- Объект для логирования с методами `info`, `warn`, `error`, `debug`. По умолчанию используется `console`. Может быть передан любой объект, реализующий интерфейс `Logger`.
104
-
105
- ```typescript
106
- interface Logger {
107
- info: (...data: unknown[]) => void;
108
- warn: (...data: unknown[]) => void;
109
- error: (...data: unknown[]) => void;
110
- debug: (...data: unknown[]) => void;
111
- }
112
- ```
113
-
114
- **Пример:**
115
-
116
- ```typescript
117
- const logger = console; // Использование стандартного console
118
-
119
- const config = {
120
- logger,
121
- // или
122
- logger: {
123
- info: (...data) => customLog('INFO', ...data),
124
- warn: (...data) => customLog('WARN', ...data),
125
- error: (...data) => customLog('ERROR', ...data),
126
- debug: (...data) => customLog('DEBUG', ...data),
127
- },
128
- };
129
- ```
130
-
131
- #### `onError?: (error, event, errorType) => void` (опциональное)
132
-
133
- Единый обработчик для всех типов ошибок в Service Worker. **Дефолтного обработчика ошибок нет** - если `onError` не передан, ошибки будут проигнорированы (не обработаны).
134
-
135
- **Параметры:**
136
-
137
- - `error: Error | any` - объект ошибки
138
- - `event: Event` - событие, в контексте которого произошла ошибка
139
- - `errorType?: ServiceWorkerErrorType` - тип ошибки (см. раздел "Обработка ошибок")
140
-
141
- **Важно:** Если `onError` не указан, ошибки в плагинах и глобальные ошибки будут проигнорированы. Для production-окружения рекомендуется всегда указывать `onError` для логирования и мониторинга ошибок.
142
-
143
- **Пример минимальной конфигурации:**
144
-
145
- ```typescript
146
- // Без onError - ошибки будут проигнорированы
147
- initServiceWorker([cachePlugin], { logger: console });
148
-
149
- // С onError - ошибки будут обработаны
150
- const logger = console; // Использование стандартного console
151
-
152
- initServiceWorker([cachePlugin], {
153
- logger,
154
- onError: (error, event, errorType) => {
155
- logger.error('Service Worker error:', error, errorType);
156
- },
157
- });
158
- ```
159
-
160
- ### Обработка ошибок
161
-
162
- Библиотека предоставляет единый обработчик для всех типов ошибок в Service Worker:
163
-
164
- ```typescript
165
- import {
166
- initServiceWorker,
167
- ServiceWorkerErrorType,
168
- } from '@budarin/pluggable-serviceworker';
169
-
170
- const logger = console; // или свой объект с методами info, warn, error, debug
171
-
172
- const config = {
173
- logger,
174
- onError: (error, event, errorType) => {
175
- logger.info(`Ошибка типа "${errorType}":`, error);
176
-
177
- switch (errorType) {
178
- case ServiceWorkerErrorType.ERROR:
179
- // JavaScript ошибки
180
- logger.error('JavaScript error:', error);
181
- break;
182
-
183
- case ServiceWorkerErrorType.MESSAGE_ERROR:
184
- // Ошибки сообщений
185
- logger.error('Message error:', error);
186
- break;
187
-
188
- case ServiceWorkerErrorType.UNHANDLED_REJECTION:
189
- // Необработанные Promise rejection
190
- logger.error('Unhandled promise rejection:', error);
191
- break;
192
-
193
- case ServiceWorkerErrorType.REJECTION_HANDLED:
194
- // Обработанные Promise rejection
195
- logger.info('Promise rejection handled:', error);
196
- break;
197
-
198
- case ServiceWorkerErrorType.PLUGIN_ERROR:
199
- // Ошибки в плагинах
200
- logger.error('Plugin error:', error);
201
- break;
202
-
203
- default:
204
- // Неизвестные типы ошибок
205
- logger.error('Unknown error type:', error);
206
-
207
- // можно даже так - отправка ошибки в аналитику
208
- fetch('/api/errors', {
209
- method: 'POST',
210
- body: JSON.stringify({
211
- error: error.message,
212
- eventType: event.type,
213
- url: event.request?.url,
214
- timestamp: Date.now(),
215
- }),
216
- }).catch(() => {
217
- // Игнорируем ошибки отправки логов
218
- });
219
- }
220
- },
221
- };
222
-
223
- initServiceWorker(
224
- [
225
- /* ваши плагины */
226
- ],
227
- config
228
- );
229
- ```
230
-
231
- ## 🔌 Интерфейс плагина
232
-
233
- Каждый плагин должен реализовывать интерфейс `ServiceWorkerPlugin`:
234
-
235
- ```typescript
236
- interface ServiceWorkerPlugin {
237
- /** Уникальное имя плагина */
238
- name: string;
239
-
240
- /** Порядок выполнения (опционально) */
241
- order?: number;
242
-
243
- /** Обработчик события install */
244
- install?: (event: ExtendableEvent) => Promise<void> | void;
245
-
246
- /** Обработчик события activate */
247
- activate?: (event: ExtendableEvent) => Promise<void> | void;
248
-
249
- /** Обработчик события fetch */
250
- fetch?: (
251
- event: FetchEvent
252
- ) => Promise<Response | undefined> | Response | undefined;
253
-
254
- /** Обработчик события message */
255
- message?: (event: SwMessageEvent) => void;
256
-
257
- /** Обработчик события sync */
258
- sync?: (event: SyncEvent) => Promise<void> | void;
259
-
260
- /** Обработчик события push */
261
- push?: (event: PushEvent) => Promise<void> | void;
262
-
263
- /** Обработчик события periodicsync */
264
- periodicsync?: (event: PeriodicSyncEvent) => Promise<void> | void;
265
- }
266
- ```
267
-
268
- ### 📝 Описание методов
269
-
270
- | Метод | Событие | Возвращает | Описание |
271
- | -------------- | -------------- | ----------------------- | --------------------------------------- |
272
- | `install` | `install` | `void` | Инициализация плагина при установке SW |
273
- | `activate` | `activate` | `void` | Активация плагина при обновлении SW |
274
- | `fetch` | `fetch` | `Response \| undefined` | Обработка сетевых запросов |
275
- | `message` | `message` | `void` | Обработка сообщений от основного потока |
276
- | `sync` | `sync` | `void` | Синхронизация данных в фоне |
277
- | `push` | `push` | `void` | Обработка push-уведомлений |
278
- | `periodicsync` | `periodicsync` | `void` | Периодические фоновые задачи |
279
-
280
- ### 🎯 Особенности обработчиков
281
-
282
- - **`fetch`**: Возвращает `Response` для завершения цепочки или `undefined` для передачи следующему плагину. Если все плагины вернули `undefined`, фреймворк вызывает `fetch(event.request)`
283
- - **Остальные**: Не возвращают значения, выполняются для всех плагинов
284
- - **Все методы опциональны** - реализуйте только нужные события
285
-
286
- ### 🔄 Обновление Service Worker (skipWaiting / clients.claim)
287
-
288
- Библиотека **не вызывает** `skipWaiting()` и `clients.claim()` — это поведение задаётся индивидуально в каждом проекте и оставлено на усмотрение плагинов. При необходимости вызывайте их в своих обработчиках `install` и `activate`:
289
-
290
- ```typescript
291
- const updatePlugin = {
292
- name: 'update-plugin',
293
- install: (event) => {
294
- self.skipWaiting();
295
- },
296
- activate: (event) => {
297
- event.waitUntil(self.clients.claim());
298
- },
299
- };
300
- ```
301
-
302
- ## 🎯 Порядок выполнения
303
-
304
- Плагины выполняются в следующем порядке:
305
-
306
- 1. **Сначала ВСЕ плагины без `order`** - в том порядке, в котором они были добавлены
307
- 2. **Затем плагины с `order`** - в порядке возрастания значений `order`
308
-
309
- ### Пример:
310
-
311
- ```typescript
312
- const plugins = [
313
- { name: 'first' }, // без order - выполняется первым
314
- { name: 'fourth', order: 2 },
315
- { name: 'second' }, // без order - выполняется вторым
316
- { name: 'third', order: 1 },
317
- { name: 'fifth' }, // без order - выполняется третьим
318
- ];
319
-
320
- // Порядок выполнения: first second fifth third fourth
321
- ```
322
-
323
- **Преимущества новой системы:**
324
-
325
- - 🎯 **Предсказуемость** - плагины без `order` всегда выполняются первыми
326
- - 🔧 **Простота** - не нужно знать, какие номера уже заняты
327
- - 📈 **Масштабируемость** - легко добавлять новые плагины в нужном порядке
328
-
329
- ## Логика выполнения обработчиков
330
-
331
- Разные типы событий Service Worker обрабатываются по-разному в зависимости от их специфики:
332
-
333
- ### 🔄 Параллельное выполнение
334
-
335
- **События:** `install`, `activate`, `message`, `sync`, `periodicsync`
336
-
337
- Все обработчики выполняются **одновременно** с помощью `Promise.all()`:
338
-
339
- ```typescript
340
- // Все плагины инициализируются параллельно
341
- const installPlugin1 = {
342
- name: 'cache-assets',
343
- install: async () => {
344
- /* кеширование ресурсов приложения*/
345
- },
346
- };
347
- const installPlugin2 = {
348
- name: 'cache-ext',
349
- install: async () => {
350
- /* кэширование вспомогательных ресурсов */
351
- },
352
- };
353
-
354
- // Оба install обработчика выполнятся одновременно
355
- ```
356
-
357
- **Почему параллельно:**
358
-
359
- - **install/activate**: Все плагины должны инициализироваться независимо
360
- - **message**: Все плагины должны получить сообщение одновременно
361
- - **sync**: Разные задачи синхронизации независимы (синхронизация данных + кеша)
362
- - **periodicsync**: Периодические задачи независимы друг от друга
363
-
364
- ### ➡️ Последовательное выполнение
365
-
366
- **События:** `fetch`, `push`
367
-
368
- Обработчики выполняются **по очереди** до первого успешного результата:
369
-
370
- #### Fetch - с прерыванием цепочки
371
-
372
- ```typescript
373
- const authPlugin = {
374
- name: 'auth',
375
- // Без order - выполняется первым
376
- fetch: async (event) => {
377
- if (needsAuth(event.request)) {
378
- return new Response('Unauthorized', { status: 401 }); // Прерывает цепочку
379
- }
380
- return undefined; // Передает следующему плагину
381
- },
382
- };
383
- ```
384
-
385
- **Почему последовательно:**
386
-
387
- - **fetch**: Нужен только один ответ, первый успешный прерывает цепочку. Если никто не вернул ответ — выполняется `fetch(event.request)`
388
- - **push**: Избегает конфликтов уведомлений, но все плагины должны обработать событие
389
-
390
- ### 📋 Сводная таблица
391
-
392
- | Событие | Выполнение | Прерывание | Причина |
393
- | -------------- | --------------- | ---------- | -------------------------------- |
394
- | `install` | Параллельно | Нет | Независимая инициализация |
395
- | `activate` | Параллельно | Нет | Независимая активация |
396
- | `fetch` | Последовательно | Да | Нужен один ответ |
397
- | `message` | Параллельно | Нет | Все получают сообщение |
398
- | `sync` | Параллельно | Нет | Независимые задачи |
399
- | `periodicsync` | Параллельно | Нет | Независимые периодические задачи |
400
- | `push` | Последовательно | Нет | Избегание конфликтов |
401
-
402
- ## 🛡️ Обработка ошибок
403
-
404
- Библиотека автоматически перехватывает и обрабатывает все типы ошибок в Service Worker через единый обработчик `config.onError` (см. раздел [Конфигурация](#-конфигурация)).
405
-
406
- **Особенности:**
407
-
408
- - **Единый обработчик** - все типы ошибок обрабатываются через `config.onError` (опциональное поле)
409
- - **Дефолтного обработчика нет** - если `onError` не указан, ошибки будут проигнорированы
410
- - **Типизированные ошибки** - третий параметр `errorType` указывает тип ошибки из `ServiceWorkerErrorType`
411
- - **Глобальные события** - автоматическая обработка `error`, `messageerror`, `unhandledrejection`, `rejectionhandled`
412
- - **Изоляция ошибок** - ошибка в одном плагине не останавливает выполнение других
413
- - **Безопасность** - ошибки в самих обработчиках ошибок логируются через `logger.error`
414
-
415
- ## Режим разработки
416
-
417
- Если нужно, чтобы в режиме разработки ни один из плагинов ничего не кэшировал и не пытался что-либо отдавать из кэша - можно использовть `import.meta.env.DEV` для Vite для условного использования кэширования.
418
-
419
- ## 📄 Лицензия
420
-
421
- MIT © Vadim Budarin
1
+ # @budarin/pluggable-serviceworker
2
+
3
+ 🔌 Расширяемый через плагины Service Worker
4
+
5
+ Библиотека для создания модульных и расширяемых Service Worker'ов с помощью системы плагинов.
6
+
7
+ ## 🚀 Почему этот пакет облегчает разработку?
8
+
9
+ Разработка Service Worker'ов традиционно сложна из-за необходимости вручную управлять множественными обработчиками событий, обработкой ошибок и порядком выполнения. Этот пакет решает эти проблемы:
10
+
11
+ ### 🔌 **Модульная архитектура**
12
+
13
+ - **Плагинная система** позволяет разбивать функциональность на независимые модули
14
+ - Каждый плагин отвечает за свою задачу (кеширование, аутентификация, уведомления)
15
+ - Легко добавлять/удалять функциональность без изменения основного кода
16
+ - Не нужно думать об инфраструктурном коде в обработчиках событий - пишите простой код не думая о сложностях кода самого сервисворкера
17
+
18
+ ### 🎯 **Управление порядком выполнения**
19
+
20
+ - **Предсказуемый порядок** - плагины без `order` выполняются первыми, затем по возрастанию `order`
21
+ - **Гибкость** - можно контролировать последовательность инициализации
22
+ - **Масштабируемость** - легко добавлять новые плагины в нужном месте
23
+
24
+ ### ⚡ **Оптимизированная логика выполнения**
25
+
26
+ - **Параллельно** для `install`, `activate`, `message`, `sync` - независимые задачи выполняются одновременно
27
+ - **Последовательно** для `fetch`, `push` - первый успешный результат прерывает цепочку
28
+ - **Производительность** - правильный выбор стратегии для каждого типа события
29
+
30
+ ### 🛡️ **Централизованная обработка ошибок**
31
+
32
+ - **Единый обработчик** для всех типов ошибок
33
+ - **Типизированные ошибки** - знаешь, что именно сломалось
34
+ - **Изоляция** - ошибка в одном плагине не ломает остальные
35
+ - **Автоматическая обработка** глобальных событий ошибок
36
+
37
+ ### 📝 **Удобное логирование**
38
+
39
+ - **Настраиваемый логгер** с разными уровнями
40
+ - **Контекстная информация** в логах
41
+ - **Отладка** становится намного проще
42
+
43
+ ## 📦 Установка
44
+
45
+ ```bash
46
+ npm install @budarin/pluggable-serviceworker
47
+ ```
48
+
49
+ или
50
+
51
+ ```bash
52
+ pnpm add @budarin/pluggable-serviceworker
53
+ ```
54
+
55
+ ## 🚀 Быстрый старт
56
+
57
+ ### Базовое использование
58
+
59
+ ```typescript
60
+ // sw.js
61
+ import {
62
+ initServiceWorker,
63
+ type ServiceWorkerPlugin,
64
+ type SwContext,
65
+ } from '@budarin/pluggable-serviceworker';
66
+
67
+ // Контекст: список ассетов для precache и имя кеша
68
+ interface PrecacheAndServeContext extends SwContext {
69
+ assets: string[];
70
+ cacheName: string;
71
+ }
72
+
73
+ const precacheAndServePlugin: ServiceWorkerPlugin<PrecacheAndServeContext> = {
74
+ name: 'precache-and-serve',
75
+
76
+ install: async (_event, context) => {
77
+ const cache = await caches.open(context.cacheName);
78
+ await cache.addAll(context.assets);
79
+ },
80
+
81
+ fetch: async (event, context) => {
82
+ const cache = await caches.open(context.cacheName);
83
+ return cache.match(event.request) ?? undefined;
84
+ },
85
+ };
86
+
87
+ // TypeScript проверит, что в options есть assets и cacheName
88
+ initServiceWorker([precacheAndServePlugin], {
89
+ logger: console,
90
+ assets: ['/', '/styles.css', '/script.js'],
91
+ cacheName: 'my-cache-v1',
92
+ });
93
+ ```
94
+
95
+ **Важно:** для `fetch` плагину не нужно самому вызывать `fetch(event.request)`, если все плагины вернули `undefined` - фреймворк сам выполняет запрос в сеть. Во все обработчики плагинов вторым аргументом передаётся **контекст** — те же данные, что вы передали в `initServiceWorker`.
96
+
97
+ ## Демо
98
+
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).
100
+
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)
102
+
103
+ ## `initServiceWorker(plugins, options)`
104
+
105
+ `initServiceWorker` — точка входа: она регистрирует обработчики событий Service Worker (`install`, `activate`, `fetch`, …) и прогоняет их через список плагинов.
106
+
107
+ - `plugins`: массив плагинов
108
+ - `options`: один общий объект с настройками/данными, который будет доступен плагинам как **контекст**
109
+
110
+ **Пример:**
111
+
112
+ ```typescript
113
+ initServiceWorker([myPlugin], {
114
+ logger: console,
115
+ // ... тут будут поля контекста, которые требуют плагины ...
116
+ });
117
+ ```
118
+
119
+ ## ⚙️ Конфигурация и контекст (options)
120
+
121
+ Функция `initServiceWorker` принимает второй параметр `options` типа `ServiceWorkerInitOptions` (контекст для плагинов + `onError` для библиотеки). В обработчики плагинов вторым аргументом передаётся **контекст** — часть этого объекта без `onError` (тип контекста — `SwContext` и ваши поля; при типизированных плагинах — пересечение требуемых ими полей).
122
+
123
+ ```typescript
124
+ interface SwContext {
125
+ logger?: Logger; // по умолчанию console
126
+ // сюда можно добавлять свои поля: version, assets, cacheName и т.д.
127
+ }
128
+
129
+ // В initServiceWorker передаётся ServiceWorkerInitOptions = SwContext + onError:
130
+ interface ServiceWorkerInitOptions extends SwContext {
131
+ onError?: (error, event, errorType?) => void; // только для библиотеки, в плагины не передаётся
132
+ }
133
+ ```
134
+
135
+ В тип контекста, который видят плагины, входит только `SwContext` и ваши поля; `onError` в этот тип не входит и используется только библиотекой.
136
+
137
+ Формируйте объект `options` в своём сервис-воркере (контекст для плагинов + при необходимости `onError`) и передавайте его в `initServiceWorker`. В плагины передаётся тот же объект как контекст — плагины получают доступ к полям контекста, а `onError` остаётся внутренним делом библиотеки.
138
+
139
+ ### Поля конфигурации
140
+
141
+ #### `logger?: Logger` (опциональное)
142
+
143
+ Объект для логирования с методами `info`, `warn`, `error`, `debug`. По умолчанию используется `console`. Может быть передан любой объект, реализующий интерфейс `Logger`.
144
+
145
+ ```typescript
146
+ interface Logger {
147
+ trace: (...data: unknown[]) => void;
148
+ debug: (...data: unknown[]) => void;
149
+ info: (...data: unknown[]) => void;
150
+ warn: (...data: unknown[]) => void;
151
+ error: (...data: unknown[]) => void;
152
+ }
153
+ ```
154
+
155
+ **Пример:**
156
+
157
+ ```typescript
158
+ const logger = console; // Использование стандартного console
159
+
160
+ const options = {
161
+ logger,
162
+ // или
163
+ 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),
169
+ },
170
+ };
171
+ ```
172
+
173
+ #### `onError?: (error, event, errorType) => void` (опциональное)
174
+
175
+ Единый обработчик для всех типов ошибок в Service Worker. **Дефолтного обработчика ошибок нет** - если `onError` не передан, ошибки будут проигнорированы (не обработаны).
176
+
177
+ **Параметры:**
178
+
179
+ - `error: Error | any` - объект ошибки
180
+ - `event: Event` - событие, в контексте которого произошла ошибка
181
+ - `errorType?: ServiceWorkerErrorType` - тип ошибки (см. раздел "Обработка ошибок")
182
+
183
+ **Важно:** Если `onError` не указан, ошибки в плагинах и глобальные ошибки будут проигнорированы. Для production-окружения рекомендуется всегда указывать `onError` для логирования и мониторинга ошибок.
184
+
185
+ **Пример минимальной конфигурации:**
186
+
187
+ ```typescript
188
+ // Без onError - ошибки будут проигнорированы
189
+ initServiceWorker([cachePlugin], {});
190
+
191
+ // С onError - ошибки будут обработаны
192
+ initServiceWorker([cachePlugin], {
193
+ logger: console,
194
+ onError: (error, event, errorType) => {
195
+ console.error('Service Worker error:', error, errorType);
196
+ },
197
+ });
198
+ ```
199
+
200
+ ### Обработка ошибок
201
+
202
+ Библиотека позволяет описать единый обработчик для всех типов ошибок в Service Worker и выполнить обработку индивидуально каждого типа ошибки. Она сама подписывается на глобальные события `error`, `messageerror`, `unhandledrejection`, `rejectionhandled`; ошибка в одном плагине не останавливает выполнение остальных. Если внутри `onError` произойдёт исключение, оно логируется через `options.logger`.
203
+
204
+ ```typescript
205
+ import {
206
+ initServiceWorker,
207
+ ServiceWorkerErrorType,
208
+ } from '@budarin/pluggable-serviceworker';
209
+
210
+ const logger = console; // или свой объект с методами info, warn, error, debug
211
+
212
+ const options = {
213
+ logger,
214
+ onError: (error, event, errorType) => {
215
+ logger.info(`Ошибка типа "${errorType}":`, error);
216
+
217
+ switch (errorType) {
218
+ case ServiceWorkerErrorType.ERROR:
219
+ // JavaScript ошибки
220
+ logger.error('JavaScript error:', error);
221
+ break;
222
+
223
+ case ServiceWorkerErrorType.MESSAGE_ERROR:
224
+ // Ошибки сообщений
225
+ logger.error('Message error:', error);
226
+ break;
227
+
228
+ case ServiceWorkerErrorType.UNHANDLED_REJECTION:
229
+ // Необработанные Promise rejection
230
+ logger.error('Unhandled promise rejection:', error);
231
+ break;
232
+
233
+ case ServiceWorkerErrorType.REJECTION_HANDLED:
234
+ // Обработанные Promise rejection
235
+ logger.info('Promise rejection handled:', error);
236
+ break;
237
+
238
+ case ServiceWorkerErrorType.PLUGIN_ERROR:
239
+ // Ошибки в плагинах
240
+ logger.error('Plugin error:', error);
241
+ break;
242
+
243
+ default:
244
+ // Неизвестные типы ошибок
245
+ logger.error('Unknown error type:', error);
246
+
247
+ // можно даже так - отправка ошибки в аналитику
248
+ fetch('/api/errors', {
249
+ method: 'POST',
250
+ body: JSON.stringify({
251
+ error: error.message,
252
+ eventType: event.type,
253
+ url: event.request?.url,
254
+ timestamp: Date.now(),
255
+ }),
256
+ }).catch(() => {
257
+ // Игнорируем ошибки отправки логов
258
+ });
259
+ }
260
+ },
261
+ };
262
+
263
+ initServiceWorker(
264
+ [
265
+ /* ваши плагины */
266
+ ],
267
+ options
268
+ );
269
+ ```
270
+
271
+ ## 🔌 Интерфейс плагина
272
+
273
+ Каждый плагин реализует интерфейс `ServiceWorkerPlugin<C>`, где `C extends SwContext` — тип контекста, который плагин ожидает. Во все обработчики вторым аргументом передаётся контекст (те же данные, что в `options` при инициализации, без `onError`).
274
+
275
+ ```typescript
276
+ interface ServiceWorkerPlugin<C extends SwContext = SwContext> {
277
+ name: string;
278
+ order?: number;
279
+
280
+ install?: (event: ExtendableEvent, context?: C) => Promise<void> | void;
281
+ activate?: (event: ExtendableEvent, context?: C) => Promise<void> | void;
282
+ fetch?: (
283
+ event: FetchEvent,
284
+ context?: C
285
+ ) => Promise<Response | undefined> | Response | undefined;
286
+ message?: (event: SwMessageEvent, context?: C) => void;
287
+ sync?: (event: SyncEvent, context?: C) => Promise<void> | void;
288
+ push?: (
289
+ event: PushEvent,
290
+ context?: C
291
+ ) =>
292
+ | Promise<PushNotificationPayload | void>
293
+ | PushNotificationPayload
294
+ | void;
295
+ periodicsync?: (
296
+ event: PeriodicSyncEvent,
297
+ context?: C
298
+ ) => Promise<void> | void;
299
+ }
300
+ ```
301
+
302
+ Плагин может объявить требуемый контекст через дженерик: `ServiceWorkerPlugin<SwContext & { assets: string[]; cacheName: string }>`. Тогда TypeScript потребует передать в `initServiceWorker` объект `options` с полями `assets` и `cacheName` (при вызове с литералом массива плагинов тип `options` выводится автоматически).
303
+
304
+ ### 📝 Описание методов
305
+
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` | Периодические фоновые задачи |
315
+
316
+ ### 🎯 Особенности обработчиков
317
+
318
+ - Во все методы вторым аргументом передаётся **контекст** (данные из объекта, переданного в `initServiceWorker`, без `onError`). Параметр можно не использовать, если плагину контекст не нужен.
319
+ - **`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`, уведомление не показывается.
321
+ - **Остальные обработчики** (`install`, `activate`, `message`, `sync`, `periodicsync`): возвращаемое значение не используется; фреймворк вызывает метод каждого плагина по очереди, цепочка не прерывается.
322
+ - **Все обработчики опциональны** — реализуйте только нужные события.
323
+
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
+ ## 🎯 Порядок выполнения
341
+
342
+ Плагины выполняются в следующем порядке:
343
+
344
+ 1. **Сначала ВСЕ плагины без `order`** - в том порядке, в котором они были добавлены
345
+ 2. **Затем плагины с `order`** - в порядке возрастания значений `order`
346
+
347
+ ### Пример:
348
+
349
+ ```typescript
350
+ const plugins = [
351
+ { name: 'first' }, // без order - выполняется первым
352
+ { name: 'fourth', order: 2 },
353
+ { name: 'second' }, // без order - выполняется вторым
354
+ { name: 'third', order: 1 },
355
+ { name: 'fifth' }, // без order - выполняется третьим
356
+ ];
357
+
358
+ // Порядок выполнения: first → second → fifth → third → fourth
359
+ ```
360
+
361
+ **Преимущества новой системы:**
362
+
363
+ - 🎯 **Предсказуемость** - плагины без `order` всегда выполняются первыми
364
+ - 🔧 **Простота** - не нужно знать, какие номера уже заняты
365
+ - 📈 **Масштабируемость** - легко добавлять новые плагины в нужном порядке
366
+
367
+ ## ⚡ Логика выполнения обработчиков
368
+
369
+ Разные типы событий Service Worker обрабатываются по-разному в зависимости от их специфики:
370
+
371
+ ### 🔄 Параллельное выполнение
372
+
373
+ **События:** `install`, `activate`, `message`, `sync`, `periodicsync`
374
+
375
+ Все обработчики выполняются **одновременно** с помощью `Promise.all()`:
376
+
377
+ ```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
+ };
391
+
392
+ // Оба install обработчика выполнятся одновременно
393
+ ```
394
+
395
+ **Почему параллельно:**
396
+
397
+ - **install/activate**: Все плагины должны инициализироваться независимо
398
+ - **message**: Все плагины должны получить сообщение одновременно
399
+ - **sync**: Разные задачи синхронизации независимы (синхронизация данных + кеша)
400
+ - **periodicsync**: Периодические задачи независимы друг от друга
401
+
402
+ ### ➡️ Последовательное выполнение
403
+
404
+ **События:** `fetch`, `push`
405
+
406
+ Обработчики выполняются **по очереди** до первого успешного результата:
407
+
408
+ #### Fetch - с прерыванием цепочки
409
+
410
+ ```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
+ };
421
+ ```
422
+
423
+ **Почему последовательно:**
424
+
425
+ - **fetch**: Нужен только один ответ, первый успешный прерывает цепочку. Если никто не вернул ответ — выполняется `fetch(event.request)`
426
+ - **push**: Плагин может вернуть данные для уведомления типа `PushNotificationPayload` | `undefined`. Библиотека один раз вызывает `showNotification` по первому вернувшемуся payload; цепочка прерывается — одно уведомление, без конфликтов.
427
+
428
+ ### 📋 Сводная таблица
429
+
430
+ | Событие | Выполнение | Прерывание | Причина |
431
+ | -------------- | --------------- | ---------- | ------------------------------------------------------ |
432
+ | `install` | Параллельно | Нет | Независимая инициализация |
433
+ | `activate` | Параллельно | Нет | Независимая активация |
434
+ | `fetch` | Последовательно | Да | Нужен один ответ |
435
+ | `message` | Параллельно | Нет | Все получают сообщение |
436
+ | `sync` | Параллельно | Нет | Независимые задачи |
437
+ | `periodicsync` | Параллельно | Нет | Независимые периодические задачи |
438
+ | `push` | Последовательно | Да | Один показ уведомления (библиотека по первому payload) |
439
+
440
+ ## Примитивы, пресеты и типовые сервис-воркеры
441
+
442
+ ### Примитивы (плагины)
443
+
444
+ Один примитив — одна операция. Импорт: `@budarin/pluggable-serviceworker/plugins`.
445
+
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 | Отдаёт из кеша, в фоне обновляет кеш из сети. |
457
+
458
+ Контекст для кеширующих примитивов: `OfflineFirstContext` (assets, cacheName, опционально claimMessageType). Импортируйте тип из основного пакета.
459
+
460
+ ### Пресеты
461
+
462
+ Комбинации примитивов (стратегии кеширования). Импорт: `@budarin/pluggable-serviceworker/presets`.
463
+
464
+ | Название | Состав | Назначение |
465
+ | ---------------- | ------------------------- | ------------------------------------ |
466
+ | **offlineFirst** | precache + serveFromCache | Статика из кеша, при промахе — сеть. |
467
+
468
+ Стратегии **networkFirst**, **staleWhileRevalidate** и др. доступны как примитивы — собирайте свой кастомный сервис-воркер из примитивов и пресетов.
469
+
470
+ ### Типовые сервис-воркеры (из коробки)
471
+
472
+ Готовые точки входа по **моменту активации** (все с кешированием offline-first). Импорт: `@budarin/pluggable-serviceworker/sw`.
473
+
474
+ | Название | Описание |
475
+ | ------------------------------------ | --------------------------------------------------------------------------------------------------- |
476
+ | **activateOnNextVisitServiceWorker** | Кеширующий SW, активируется при следующем визите страницы. |
477
+ | **activateImmediatelyServiceWorker** | Кеширующий SW, активируется сразу (skipWaiting + claim). |
478
+ | **activateOnSignalServiceWorker** | Кеширующий SW, активируется по сигналу со страницы (сообщение с типом из options.claimMessageType). |
479
+
480
+ Пример использования типового SW:
481
+
482
+ ```typescript
483
+ // sw.js — точка входа вашего сервис-воркера
484
+ import { activateOnNextVisitServiceWorker } from '@budarin/pluggable-serviceworker/sw';
485
+
486
+ activateOnNextVisitServiceWorker({
487
+ assets: ['/', '/styles.css', '/script.js'],
488
+ cacheName: 'my-cache-v1',
489
+ logger: console,
490
+ onError: (err, event, type) => console.error(type, err),
491
+ });
492
+ ```
493
+
494
+ На странице регистрируйте этот файл: `navigator.serviceWorker.register('/sw.js')` (или путь, по которому сборка отдаёт ваш sw.js).
495
+
496
+ ## Режим разработки
497
+
498
+ Если нужно, чтобы в режиме разработки ни один из плагинов ничего не кэшировал и не пытался что-либо отдавать из кэша - в плагине и сервисворкере можно использовть `import.meta.env.DEV` для Vite для условного использования кэширования.
499
+
500
+ ## 📄 Лицензия
501
+
502
+ MIT © Vadim Budarin