@sweidos/eidos 1.0.16 → 1.0.19

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
@@ -72,7 +72,7 @@ pnpm add @sweidos/eidos
72
72
  cp node_modules/@sweidos/eidos/dist/eidos-sw.js public/eidos-sw.js
73
73
  ```
74
74
 
75
- > **Vite users** — automate this with the [Vite plugin snippet](#vite-plugin).
75
+ > **Vite users** — use the [first-class Vite plugin](#vite-plugin) to automate this.
76
76
 
77
77
  ### 3. Wrap your app
78
78
 
@@ -115,7 +115,16 @@ export const createOrder = action(
115
115
  ### 5. Use in components
116
116
 
117
117
  ```tsx
118
- // TanStack Query
118
+ // TanStack Query — first-class hooks
119
+ import { useEidosQuery, useEidosMutation } from '@sweidos/eidos/query'
120
+
121
+ const { data, isPending } = useEidosQuery<Product[]>(products)
122
+
123
+ const mutation = useEidosMutation(createOrder, {
124
+ invalidates: [products], // clears cache + refetches on success
125
+ })
126
+
127
+ // Or with plain useQuery
119
128
  const { data } = useQuery(products.query<Product[]>())
120
129
 
121
130
  // Or plain async
@@ -145,6 +154,15 @@ const handle = resource('/api/products', {
145
154
  cacheName?: string, // custom Cache Storage bucket (default: 'eidos-resources-v1')
146
155
  maxAge?: number, // TTL in ms — expired entries are re-fetched from network
147
156
  })
157
+
158
+ // URL patterns — SW intercepts all matching requests automatically
159
+ resource('/api/products/*', { offline: true }) // single segment: /api/products/123
160
+ resource('/api/products/**', { offline: true }) // multi-segment: /api/products/123/reviews
161
+ resource('/api/users/:id', { offline: true }) // named segment: /api/users/abc
162
+
163
+ // Cross-origin resources — pass the full URL (including origin)
164
+ resource('https://api.example.com/products', { offline: true })
165
+ resource('https://cdn.example.com/assets/*', { offline: true }) // patterns work too
148
166
  ```
149
167
 
150
168
  **Auto-selected strategy:**
@@ -287,6 +305,79 @@ const state = useEidos()
287
305
 
288
306
  ---
289
307
 
308
+ ### Vue / Svelte / Vanilla JS Stores
309
+
310
+ Framework-agnostic reactive stores — no React dependency, zero extra peer deps. These implement the [Svelte store contract](https://svelte.dev/docs/svelte-components#script-4-prefix-stores-with-$-to-access-their-values) (`subscribe(run): unsubscribe`) so they work natively with Svelte's `$` prefix. They also wire up cleanly in Vue composables and plain JS.
311
+
312
+ ```ts
313
+ import {
314
+ eidosQueue, eidosStatus, eidosQueueStats,
315
+ eidosResource, eidosAction, eidosStore,
316
+ } from '@sweidos/eidos'
317
+ ```
318
+
319
+ **Svelte:**
320
+
321
+ ```svelte
322
+ <script>
323
+ import { eidosQueue, eidosStatus, eidosQueueStats, eidosResource } from '@sweidos/eidos'
324
+ // Use $ prefix — Svelte auto-subscribes and unsubscribes
325
+ </script>
326
+
327
+ <p>Online: {$eidosStatus.isOnline}</p>
328
+ <p>Pending: {$eidosQueueStats.pending}</p>
329
+ <p>Cache hits: {$eidosResource('/api/products')?.cacheHits ?? 0}</p>
330
+
331
+ {#each $eidosQueue as item}
332
+ <div>{item.actionName} — {item.status}</div>
333
+ {/each}
334
+ ```
335
+
336
+ **Vue (Composition API):**
337
+
338
+ ```ts
339
+ import { ref, onUnmounted } from 'vue'
340
+ import { eidosStatus, eidosQueue } from '@sweidos/eidos'
341
+
342
+ export function useEidosStatusVue() {
343
+ const status = ref(eidosStatus.getState())
344
+ const unsub = eidosStatus.subscribe((v) => { status.value = v })
345
+ onUnmounted(unsub)
346
+ return status
347
+ }
348
+
349
+ export function useEidosQueueVue() {
350
+ const queue = ref(eidosQueue.getState())
351
+ const unsub = eidosQueue.subscribe((v) => { queue.value = v })
352
+ onUnmounted(unsub)
353
+ return queue
354
+ }
355
+ ```
356
+
357
+ **Vanilla JS:**
358
+
359
+ ```ts
360
+ import { eidosStatus, eidosResource } from '@sweidos/eidos'
361
+
362
+ const unsub = eidosStatus.subscribe(({ isOnline }) => {
363
+ document.title = isOnline ? 'App' : 'App (offline)'
364
+ })
365
+
366
+ // Read current value once without subscribing
367
+ const hits = eidosResource('/api/products').getState()?.cacheHits ?? 0
368
+ ```
369
+
370
+ | Store | Type | Emits when |
371
+ |-------|------|-----------|
372
+ | `eidosQueue` | `ActionQueueItem[]` | Any queue mutation |
373
+ | `eidosStatus` | `{ isOnline, swStatus, swError }` | Online or SW status changes |
374
+ | `eidosQueueStats` | `{ pending, failed, replaying, total }` | Any queue mutation |
375
+ | `eidosResource(url)` | `ResourceEntry \| undefined` | Resource registered or updated |
376
+ | `eidosAction(id)` | `ActionQueueItem \| undefined` | Item status changes or removal |
377
+ | `eidosStore` | `EidosStore` | Any state change |
378
+
379
+ ---
380
+
290
381
  ### `setOfflineSimulation(enabled)`
291
382
 
292
383
  Toggle offline simulation without physically disconnecting the network.
@@ -300,6 +391,21 @@ setOfflineSimulation(false) // restore normal behaviour
300
391
 
301
392
  ---
302
393
 
394
+ ### `isBgSyncSupported()`
395
+
396
+ Returns `true` when the active SW registration supports the Background Sync API (Chrome 49+, Edge 79+, Safari 16+). Use to conditionally surface sync status in your UI.
397
+
398
+ ```ts
399
+ import { isBgSyncSupported } from '@sweidos/eidos'
400
+
401
+ if (isBgSyncSupported()) {
402
+ // browser will fire 'eidos-queue-replay' sync tag when connectivity returns,
403
+ // even if the user briefly navigated away from the page
404
+ }
405
+ ```
406
+
407
+ ---
408
+
303
409
  ## Performance
304
410
 
305
411
  Performance is a first-class concern in Eidos. Every design decision optimises for low overhead.
@@ -369,6 +475,7 @@ Performance is a first-class concern in Eidos. Every design decision optimises f
369
475
  | `EIDOS_CACHE_UPDATED` | Cache entry refreshed from network |
370
476
  | `EIDOS_NETWORK_ERROR` | Network request failed |
371
477
  | `EIDOS_CACHE_CLEARED` | Cache was cleared |
478
+ | `EIDOS_BACKGROUND_SYNC` | Browser fired `sync` event — runtime calls `replayQueue()` |
372
479
 
373
480
  ---
374
481
 
@@ -401,26 +508,96 @@ eidos/
401
508
 
402
509
  ## Vite Plugin
403
510
 
404
- Automatically copy `eidos-sw.js` into `public/` on build:
511
+ `@sweidos/eidos` ships a first-class Vite plugin via the `@sweidos/eidos/vite` subpath. It automatically copies `eidos-sw.js` from the installed package into your `public/` directory on every build and dev-server start — keeping the SW in sync with the installed version.
405
512
 
406
513
  ```ts
407
514
  // vite.config.ts
408
- import { copyFileSync } from 'fs'
409
- import { resolve } from 'path'
410
-
411
- function eidosPlugin() {
412
- return {
413
- name: 'eidos-sw',
414
- buildStart() {
415
- copyFileSync(
416
- resolve('./node_modules/@sweidos/eidos/dist/eidos-sw.js'),
417
- resolve('./public/eidos-sw.js'),
418
- )
515
+ import { eidos } from '@sweidos/eidos/vite'
516
+ import { defineConfig } from 'vite'
517
+
518
+ export default defineConfig({
519
+ plugins: [eidos()],
520
+ })
521
+ ```
522
+
523
+ **Options:**
524
+
525
+ ```ts
526
+ eidos({
527
+ swDest: 'public/eidos-sw.js', // default — relative to project root
528
+ })
529
+ ```
530
+
531
+ No more manual `cp` step. The plugin runs on `buildStart` (prod builds) and `configureServer` (dev).
532
+
533
+ ---
534
+
535
+ ## TanStack Query Integration
536
+
537
+ `@sweidos/eidos/query` provides first-class hooks for [TanStack Query v5](https://tanstack.com/query/latest). Requires `@tanstack/react-query` — already optional in Eidos, just install it.
538
+
539
+ ### Setup (once)
540
+
541
+ ```ts
542
+ // main.tsx
543
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
544
+ import { withEidosQueryClient } from '@sweidos/eidos/query'
545
+
546
+ const queryClient = new QueryClient()
547
+ withEidosQueryClient(queryClient) // bridges handle.invalidate() → TQ cache
548
+
549
+ root.render(
550
+ <QueryClientProvider client={queryClient}>
551
+ <EidosProvider swPath="/eidos-sw.js">
552
+ <App />
553
+ </EidosProvider>
554
+ </QueryClientProvider>
555
+ )
556
+ ```
557
+
558
+ ### `useEidosQuery(handle, options?)`
559
+
560
+ Wraps `useQuery` with Eidos-smart defaults:
561
+ - `networkMode: 'always'` — Eidos owns offline; queries run even when `navigator.onLine` is false
562
+ - `retry: false` — Eidos handles retries at the SW/replay layer
563
+
564
+ ```tsx
565
+ import { useEidosQuery } from '@sweidos/eidos/query'
566
+
567
+ function ProductList() {
568
+ const { data, isPending, isError } = useEidosQuery<Product[]>(products)
569
+ // ...
570
+ }
571
+ ```
572
+
573
+ ### `useEidosMutation(handle, options?)`
574
+
575
+ Wraps `useMutation` for a single-argument action handle:
576
+ - `networkMode: 'always'` — action queues offline automatically
577
+ - `invalidates` — clears Eidos cache + invalidates TQ entries on success
578
+
579
+ ```tsx
580
+ import { useEidosMutation } from '@sweidos/eidos/query'
581
+
582
+ function OrderForm() {
583
+ const mutation = useEidosMutation(createOrder, {
584
+ invalidates: [products], // refetch product list after order
585
+ onSuccess(data) {
586
+ if ('queued' in data) toast('Saved offline — will sync when back online')
587
+ else toast(`Order #${data.id} created!`)
419
588
  },
420
- }
589
+ })
590
+
591
+ return <button onClick={() => mutation.mutate({ productId: 1, qty: 2 })}>Buy</button>
421
592
  }
422
593
  ```
423
594
 
595
+ ### `withEidosQueryClient(client)`
596
+
597
+ Registers a `QueryClient` with Eidos. After calling this:
598
+ - `handle.invalidate()` also calls `queryClient.invalidateQueries({ queryKey: ['eidos', url] })`
599
+ - Both systems stay in sync automatically, even when cache is cleared outside of mutations
600
+
424
601
  ---
425
602
 
426
603
  ## Known Limitations
@@ -428,7 +605,7 @@ function eidosPlugin() {
428
605
  | Limitation | Detail |
429
606
  |------------|--------|
430
607
  | GET-only caching | SW intercepts `GET` only. `POST`/`PUT`/`DELETE` are not cached (but *are* queued via `action()`). |
431
- | Pathname matching | Resources match by pathname. `/api/products?page=2` and `/api/products` share the same SW rule but are cached separately. |
608
+ | Query string ignored | Resources match by pathname (or full URL for cross-origin). `/api/products?page=2` and `/api/products` share the same SW rule but are cached as separate entries. |
432
609
  | Module-scope actions | `action()` must be called at module scope so functions are registered before a page reload triggers queue replay. |
433
610
  | Single SW | `EidosProvider` assumes one SW at `/eidos-sw.js`. Multiple registrations are unsupported. |
434
611
 
@@ -440,12 +617,12 @@ function eidosPlugin() {
440
617
  - [x] Exponential backoff with jitter for queue replay
441
618
  - [x] Per-resource `cacheName` override
442
619
  - [x] `resource.unregister()` for cleanup
443
- - [ ] URL pattern matching (wildcards, regex)
444
- - [ ] Cross-origin resource support
445
- - [ ] Background Sync API integration
446
- - [ ] Vite plugin (first-class, published separately)
447
- - [ ] Vue / Svelte bindings
448
- - [ ] TanStack Query integration package
620
+ - [x] URL pattern matching (`*`, `**`, `:param`)
621
+ - [x] Cross-origin resource support
622
+ - [x] Background Sync API integration
623
+ - [x] Vite plugin (`@sweidos/eidos/vite` subpath — ships in the main package)
624
+ - [x] Vue / Svelte bindings (framework-agnostic reactive stores)
625
+ - [x] TanStack Query integration (`@sweidos/eidos/query` subpath — `useEidosQuery`, `useEidosMutation`, `withEidosQueryClient`)
449
626
 
450
627
  ---
451
628