@sweidos/eidos 2.1.0 → 2.3.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.
- package/README.md +290 -22
- package/dist/action.d.ts +22 -0
- package/dist/action.js +47 -47
- package/dist/action.js.map +1 -1
- package/dist/async-storage-adapter.d.ts +25 -0
- package/dist/debug.d.ts +46 -0
- package/dist/debug.js +43 -0
- package/dist/debug.js.map +1 -0
- package/dist/devtools.js +350 -21
- package/dist/eidos-sw.js +60 -19
- package/dist/eidos.cjs +5 -5
- package/dist/eidos.cjs.map +1 -1
- package/dist/idb.d.ts +10 -0
- package/dist/index.d.ts +20 -586
- package/dist/index.js +47 -41
- package/dist/internal/url-base64.d.ts +2 -0
- package/dist/query.d.ts +1 -2
- package/dist/queue-storage.d.ts +12 -0
- package/dist/queue-sync.d.ts +32 -0
- package/dist/react/Provider.d.ts +16 -0
- package/dist/react/ProviderRN.d.ts +0 -1
- package/dist/react/hooks.d.ts +51 -0
- package/dist/react/hooks.js +30 -27
- package/dist/react/hooks.js.map +1 -1
- package/dist/replay.d.ts +15 -0
- package/dist/resource.d.ts +32 -0
- package/dist/resource.js +80 -78
- package/dist/resource.js.map +1 -1
- package/dist/runtime-rn.d.ts +0 -1
- package/dist/runtime.d.ts +39 -0
- package/dist/runtime.js +32 -24
- package/dist/runtime.js.map +1 -1
- package/dist/store-slices.d.ts +26 -0
- package/dist/store-slices.js +31 -20
- package/dist/store-slices.js.map +1 -1
- package/dist/store.d.ts +15 -0
- package/dist/store.js +22 -19
- package/dist/store.js.map +1 -1
- package/dist/stores.d.ts +64 -0
- package/dist/stores.js +31 -22
- package/dist/stores.js.map +1 -1
- package/dist/sveltekit.d.ts +0 -1
- package/dist/sw-bridge.d.ts +24 -0
- package/dist/sw-bridge.js +69 -54
- package/dist/sw-bridge.js.map +1 -1
- package/dist/testing.cjs +3 -2
- package/dist/testing.d.ts +1 -2
- package/dist/testing.js +3 -2
- package/dist/types.d.ts +305 -0
- package/dist/types.js +19 -8
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/vite.d.ts +0 -1
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@sweidos/eidos)
|
|
5
5
|
[](https://bundlejs.com/?q=@sweidos/eidos)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
|
-
[](https://github.com/sweidos/eidos/actions)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
|
-
> **
|
|
10
|
+
> **Never lose a write.**
|
|
11
11
|
|
|
12
|
-
Declare what your app needs offline. Eidos picks the cache strategy, registers the Service Worker, and persists your action queue to IndexedDB —
|
|
12
|
+
Declare what your app needs offline. Eidos picks the cache strategy, registers the Service Worker, and persists your action queue to IndexedDB — with idempotency keys and cross-tab replay locks built in, so a queued mutation runs exactly once.
|
|
13
13
|
|
|
14
14
|
```ts
|
|
15
15
|
import { resource, action } from '@sweidos/eidos';
|
|
@@ -135,16 +135,19 @@ if ('queued' in result) {
|
|
|
135
135
|
|
|
136
136
|
## Framework support
|
|
137
137
|
|
|
138
|
-
| Framework
|
|
139
|
-
|
|
|
140
|
-
| **React**
|
|
141
|
-
| **Next.js App Router**
|
|
142
|
-
| **
|
|
143
|
-
| **
|
|
144
|
-
| **
|
|
145
|
-
| **
|
|
146
|
-
| **
|
|
147
|
-
| **
|
|
138
|
+
| Framework | Import path | Notes |
|
|
139
|
+
| -------------------------- | ----------------------------- | -------------------------------------------------------------- |
|
|
140
|
+
| **React** | `@sweidos/eidos` | Hooks + `EidosProvider` |
|
|
141
|
+
| **Next.js App Router** | `@sweidos/eidos/nextjs` | Pre-marked `'use client'` — no wrapper needed |
|
|
142
|
+
| **Next.js Server Actions** | `@sweidos/next` | `serverAction()` neverLose wrapper + idempotency context |
|
|
143
|
+
| **SvelteKit** | `@sweidos/eidos/sveltekit` | `initEidosSvelteKit()` in `onMount`, framework-agnostic stores |
|
|
144
|
+
| **Vue** | `@sweidos/eidos` | Framework-agnostic stores via `eidosStatus.subscribe()` |
|
|
145
|
+
| **React Native** | `@sweidos/eidos/react-native` | AsyncStorage-backed queue, same `action()` API |
|
|
146
|
+
| **Vanilla JS** | `@sweidos/eidos` | `eidosStatus`, `eidosQueue`, `eidosQueueStats` stores |
|
|
147
|
+
| **Vite** | `@sweidos/eidos/vite` | Plugin auto-copies `eidos-sw.js` on every build |
|
|
148
|
+
| **CRDT merge (Yjs)** | `@sweidos/crdt-yjs` | `createYjsMergeResolver()` for `conflict.strategy: 'merge'` |
|
|
149
|
+
| **TanStack Query** | `@sweidos/eidos/query` | `useEidosQuery`, `useEidosMutation`, `withEidosQueryClient` |
|
|
150
|
+
| **Tauri / Electron** | `@sweidos/sqlite-storage` | SQLite-backed `QueueStorage`, same `action()` API |
|
|
148
151
|
|
|
149
152
|
---
|
|
150
153
|
|
|
@@ -159,10 +162,14 @@ const products = resource('/api/products', {
|
|
|
159
162
|
offline: true, // enable SW interception + caching
|
|
160
163
|
strategy?: 'cache-first' | 'stale-while-revalidate' | 'network-first',
|
|
161
164
|
cacheName?: string, // custom cache bucket
|
|
162
|
-
maxAge?: number, // TTL in ms —
|
|
165
|
+
maxAge?: number, // TTL in ms — enforced by the SW on all requests (not just handle.fetch())
|
|
166
|
+
maxEntries?: number, // max cache entries; oldest evicted (FIFO) when exceeded
|
|
163
167
|
version?: string | number, // bump when the response shape changes —
|
|
164
168
|
// appended to cacheName (e.g. 'eidos-resources-v1-v2')
|
|
165
|
-
// so old-shaped cache entries aren't served
|
|
169
|
+
// so old-shaped cache entries aren't served.
|
|
170
|
+
// NOTE: this is separate from the SW-internal CACHE_VERSION
|
|
171
|
+
// (bumped only on Eidos releases to purge old cache buckets).
|
|
172
|
+
// Bump `version` for your data shape; Eidos bumps CACHE_VERSION.
|
|
166
173
|
})
|
|
167
174
|
|
|
168
175
|
await products.fetch() // Promise<Response>
|
|
@@ -194,6 +201,19 @@ await productPattern.invalidate(); // clear all cached entries matching the patt
|
|
|
194
201
|
productPattern.unregister();
|
|
195
202
|
```
|
|
196
203
|
|
|
204
|
+
| Token | Matches |
|
|
205
|
+
| -------- | ------------------------------------------------------------ |
|
|
206
|
+
| `*` | One path segment — `/api/products/*` ↔ `/api/products/4` |
|
|
207
|
+
| `**` | Any number of segments — `https://cdn.example.com/assets/**` |
|
|
208
|
+
| `:param` | A named segment — `/api/users/:id/orders` |
|
|
209
|
+
|
|
210
|
+
Pass the full URL (including origin) for cross-origin resources, e.g.
|
|
211
|
+
`resourcePattern('https://cdn.example.com/assets/**', { offline: true })`.
|
|
212
|
+
|
|
213
|
+
See it live in the [playground docs → Examples → URL patterns, one
|
|
214
|
+
registration per
|
|
215
|
+
family](https://sweidos.vercel.app/docs/examples#url-patterns-one-registration-per-family).
|
|
216
|
+
|
|
197
217
|
### `action(fn, config)`
|
|
198
218
|
|
|
199
219
|
```ts
|
|
@@ -223,6 +243,62 @@ await cancelByIdempotencyKey(idempotencyKey) // true if cancelled/removed
|
|
|
223
243
|
await requeueItem(queueItemId) // true if it was 'failed'
|
|
224
244
|
```
|
|
225
245
|
|
|
246
|
+
### Conflict resolution
|
|
247
|
+
|
|
248
|
+
A `neverLose` action can sit in the queue for a while — by the time it
|
|
249
|
+
replays, the world may have moved on (the requested stock sold out, the
|
|
250
|
+
record was deleted, etc.). `conflict` decides what happens when a replay gets
|
|
251
|
+
a 4xx response, instead of retrying forever or silently dropping the write:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
class StockConflictError extends Error {
|
|
255
|
+
status = 409;
|
|
256
|
+
constructor(public available: number) {
|
|
257
|
+
super('insufficient stock');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export const reserveStock = action(
|
|
262
|
+
async (payload: { productId: number; quantity: number }) => {
|
|
263
|
+
const res = await fetch('/api/inventory', {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
body: JSON.stringify(payload),
|
|
266
|
+
});
|
|
267
|
+
if (res.status === 409) {
|
|
268
|
+
const { available } = await res.json();
|
|
269
|
+
throw new StockConflictError(available);
|
|
270
|
+
}
|
|
271
|
+
if (!res.ok) throw new Error('Reservation failed');
|
|
272
|
+
return res.json();
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
reliability: 'neverLose',
|
|
276
|
+
name: 'reserveStock',
|
|
277
|
+
conflict: {
|
|
278
|
+
strategy: 'custom',
|
|
279
|
+
resolve: ({ error, args, attempt }) => {
|
|
280
|
+
if (error instanceof StockConflictError && error.available > 0) {
|
|
281
|
+
const [payload] = args;
|
|
282
|
+
// Rewrite the queued args and retry with what's actually available
|
|
283
|
+
return { resolved: [{ ...payload, quantity: error.available }] };
|
|
284
|
+
}
|
|
285
|
+
return 'skip'; // nothing left to reserve — drop the write
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
| Strategy | Behavior on 4xx replay |
|
|
293
|
+
| ------------ | --------------------------------------------------------------------- |
|
|
294
|
+
| `serverWins` | Drop the queued item — the server's current state is authoritative. |
|
|
295
|
+
| `clientWins` | Keep retrying — the write should eventually succeed. |
|
|
296
|
+
| `merge` | Call `resolve(ctx)`; typically used to combine client + server state. |
|
|
297
|
+
| `custom` | Call `resolve(ctx)`; return `'retry'`, `'skip'`, or `{ resolved }`. |
|
|
298
|
+
|
|
299
|
+
See it live in the [playground docs → Examples → Conflict resolution on
|
|
300
|
+
replay](https://sweidos.vercel.app/docs/examples#conflict-resolution-on-replay).
|
|
301
|
+
|
|
226
302
|
### React hooks
|
|
227
303
|
|
|
228
304
|
```ts
|
|
@@ -231,6 +307,9 @@ const { pending, failed } = useEidosQueueStats();
|
|
|
231
307
|
const entry = useEidosResource('/api/products');
|
|
232
308
|
const item = useEidosAction(queuedResult.id);
|
|
233
309
|
useEidosOnDrain(() => toast('All offline actions synced!'));
|
|
310
|
+
|
|
311
|
+
// Cumulative neverLose outcome counters (queued/succeeded/failed/retried/conflicted/cancelled)
|
|
312
|
+
const { queued, succeeded, failed: failedCount } = useEidosReliabilityStats();
|
|
234
313
|
```
|
|
235
314
|
|
|
236
315
|
### Framework-agnostic stores
|
|
@@ -241,8 +320,105 @@ eidosStatus.subscribe(({ isOnline }) => { ... })
|
|
|
241
320
|
eidosQueue.subscribe((queue) => { ... })
|
|
242
321
|
eidosQueueStats.getState() // { pending, failed, replaying, total }
|
|
243
322
|
eidosResource('/api/products').getState() // ResourceEntry | undefined
|
|
323
|
+
onQueueDrain(() => toast('All offline actions synced!')) // returns unsubscribe
|
|
324
|
+
eidosReliabilityStats.getState() // { queued, succeeded, failed, retried, conflicted, cancelled }
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Reliability telemetry
|
|
328
|
+
|
|
329
|
+
Opt in to periodic reporting of cumulative `neverLose` queue outcomes — wire it
|
|
330
|
+
up to your analytics backend:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
initEidos({
|
|
334
|
+
onReliabilityReport: (stats) => analytics.track('eidos_reliability', stats),
|
|
335
|
+
reliabilityReportInterval: 60_000, // default
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
The same counters are visible live in `<EidosDevtools />` under the
|
|
340
|
+
"Reliability" tab.
|
|
341
|
+
|
|
342
|
+
### Handling SW updates
|
|
343
|
+
|
|
344
|
+
By default, when a new service worker is available it activates immediately
|
|
345
|
+
(`skipWaiting: true`). This matches standard PWA behaviour but can interrupt
|
|
346
|
+
in-flight requests on pages that are mid-navigation.
|
|
347
|
+
|
|
348
|
+
Opt into the **toast-then-reload** pattern with `skipWaiting: false`:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { initEidos, triggerSwUpdate } from '@sweidos/eidos';
|
|
352
|
+
|
|
353
|
+
initEidos({
|
|
354
|
+
skipWaiting: false,
|
|
355
|
+
onUpdateAvailable: (_registration) => {
|
|
356
|
+
// Show a toast, banner, or dialog — then call triggerSwUpdate() when the
|
|
357
|
+
// user confirms they're ready to reload.
|
|
358
|
+
showToast({
|
|
359
|
+
message: 'App update ready',
|
|
360
|
+
action: { label: 'Reload', onClick: triggerSwUpdate },
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`triggerSwUpdate()` tells the waiting service worker to activate, then the
|
|
367
|
+
browser reloads the page. With `skipWaiting: true` (default) `onUpdateAvailable`
|
|
368
|
+
is never called and `triggerSwUpdate()` is not needed.
|
|
369
|
+
|
|
370
|
+
> **Tip**: avoid calling `triggerSwUpdate()` while `neverLose` actions are
|
|
371
|
+
> mid-replay. The replay coordination (BroadcastChannel + Web Locks) survives
|
|
372
|
+
> SW activation, but triggering an update during an active replay pass adds
|
|
373
|
+
> unnecessary churn. Wait until the queue drains or use `waitForQueueDrain()`
|
|
374
|
+
> from `@sweidos/eidos/testing` in tests.
|
|
375
|
+
|
|
376
|
+
### Queue management
|
|
377
|
+
|
|
378
|
+
Inspect and manage the offline action queue directly — handy for "pending
|
|
379
|
+
changes" panels, manual retry buttons, or a "discard my offline edits" action:
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
import {
|
|
383
|
+
useEidosQueue,
|
|
384
|
+
cancelByIdempotencyKey,
|
|
385
|
+
requeueItem,
|
|
386
|
+
clearQueue,
|
|
387
|
+
replayQueue,
|
|
388
|
+
} from '@sweidos/eidos';
|
|
389
|
+
|
|
390
|
+
function QueuePanel() {
|
|
391
|
+
const queue = useEidosQueue(); // live list of pending/replaying/failed items
|
|
392
|
+
|
|
393
|
+
return queue.map((item) => (
|
|
394
|
+
<li key={item.id}>
|
|
395
|
+
{item.actionName} — {item.status}
|
|
396
|
+
|
|
397
|
+
{/* Drop a write before it ever reaches the server */}
|
|
398
|
+
{item.status === 'pending' && (
|
|
399
|
+
<button onClick={() => cancelByIdempotencyKey(item.idempotencyKey)}>
|
|
400
|
+
Cancel
|
|
401
|
+
</button>
|
|
402
|
+
)}
|
|
403
|
+
|
|
404
|
+
{/* Reset a failed item to 'pending' and replay it */}
|
|
405
|
+
{item.status === 'failed' && (
|
|
406
|
+
<button onClick={() => requeueItem(item.id)}>Retry</button>
|
|
407
|
+
)}
|
|
408
|
+
</li>
|
|
409
|
+
));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Drop every queued write — e.g. on "discard offline changes"
|
|
413
|
+
await clearQueue();
|
|
414
|
+
|
|
415
|
+
// Force a replay pass — normally triggered automatically on reconnect
|
|
416
|
+
await replayQueue();
|
|
244
417
|
```
|
|
245
418
|
|
|
419
|
+
See it live in the [playground docs → Examples → Queue management &
|
|
420
|
+
reliability stats](https://sweidos.vercel.app/docs/examples#queue-management-reliability-stats).
|
|
421
|
+
|
|
246
422
|
---
|
|
247
423
|
|
|
248
424
|
## TanStack Query
|
|
@@ -342,6 +518,10 @@ for client-side routing; otherwise the SW opens `data.url` directly.
|
|
|
342
518
|
|
|
343
519
|
## Testing
|
|
344
520
|
|
|
521
|
+
`@sweidos/eidos/testing` runs entirely at the JS layer — no real Service
|
|
522
|
+
Worker needed — and gives Vitest/Jest/Playwright direct control over online
|
|
523
|
+
state, the action queue, and the resource cache.
|
|
524
|
+
|
|
345
525
|
```ts
|
|
346
526
|
import {
|
|
347
527
|
mockOffline,
|
|
@@ -357,29 +537,82 @@ import {
|
|
|
357
537
|
beforeEach(() => resetEidos());
|
|
358
538
|
|
|
359
539
|
it('queues action while offline', async () => {
|
|
360
|
-
mockOffline();
|
|
361
|
-
await createOrder({ productId: 1,
|
|
540
|
+
mockOffline({ stubFetch: true });
|
|
541
|
+
await createOrder({ productId: 1, quantity: 2 });
|
|
362
542
|
expect(getEidosState().queue).toHaveLength(1);
|
|
543
|
+
expect(getEidosState().queue[0].actionName).toBe('createOrder');
|
|
363
544
|
});
|
|
364
545
|
|
|
365
546
|
it('replays on reconnect', async () => {
|
|
366
547
|
mockOffline();
|
|
367
|
-
await createOrder({ productId: 1,
|
|
548
|
+
await createOrder({ productId: 1, quantity: 2 });
|
|
549
|
+
|
|
550
|
+
// Forces isOnline = true and replays immediately
|
|
368
551
|
const result = await drainQueue();
|
|
369
552
|
expect(result.succeeded).toBe(1);
|
|
553
|
+
|
|
554
|
+
// ...or for code that replays itself on the 'online' event:
|
|
555
|
+
await waitForQueueDrain({ timeout: 2000 });
|
|
556
|
+
expect(getEidosState().queue).toHaveLength(0);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('caches GET responses for offline use', async () => {
|
|
560
|
+
await products.json();
|
|
561
|
+
const cached = await getCachedEntry('/api/products');
|
|
562
|
+
expect(cached).toBeDefined();
|
|
370
563
|
});
|
|
564
|
+
|
|
565
|
+
afterEach(() => clearEidosCache());
|
|
371
566
|
```
|
|
372
567
|
|
|
373
568
|
---
|
|
374
569
|
|
|
375
570
|
## OpenAPI codegen
|
|
376
571
|
|
|
572
|
+
`eidos-gen` reads an OpenAPI 3.x spec (JSON or YAML) and writes typed
|
|
573
|
+
`resource()` + `action()` declarations — request/response interfaces from
|
|
574
|
+
`$ref` schemas, `{id}` → `:id` path-param conversion, and DELETE body
|
|
575
|
+
omission.
|
|
576
|
+
|
|
377
577
|
```bash
|
|
378
|
-
npx eidos-gen openapi.json
|
|
379
|
-
|
|
578
|
+
npx eidos-gen openapi.json --out src/lib/eidos.generated.ts
|
|
579
|
+
|
|
580
|
+
eidos-gen: reading openapi.json
|
|
581
|
+
eidos-gen: wrote src/lib/eidos.generated.ts
|
|
582
|
+
2 resource(s), 2 action(s)
|
|
583
|
+
2 type(s)
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
// eidos.generated.ts
|
|
588
|
+
import { resource, action } from '@sweidos/eidos';
|
|
589
|
+
|
|
590
|
+
export interface Product {
|
|
591
|
+
id: number;
|
|
592
|
+
name: string;
|
|
593
|
+
tags?: string[];
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export const listProducts = resource('/products', { offline: true });
|
|
597
|
+
|
|
598
|
+
export const createProduct = action(
|
|
599
|
+
async (payload: Product): Promise<Product> => {
|
|
600
|
+
const res = await fetch('/products', {
|
|
601
|
+
method: 'POST',
|
|
602
|
+
headers: { 'Content-Type': 'application/json' },
|
|
603
|
+
body: JSON.stringify(payload),
|
|
604
|
+
});
|
|
605
|
+
return res.json();
|
|
606
|
+
},
|
|
607
|
+
{ reliability: 'neverLose', name: 'createProduct' },
|
|
608
|
+
);
|
|
380
609
|
```
|
|
381
610
|
|
|
382
|
-
|
|
611
|
+
| Flag | Effect |
|
|
612
|
+
| -------------- | ------------------------------------------------------------ |
|
|
613
|
+
| `--out, -o` | Output file path (default: `eidos.generated.ts`) |
|
|
614
|
+
| `--no-offline` | Set `offline: false` on every generated `resource()` |
|
|
615
|
+
| `--eidos` | Import path for `@sweidos/eidos` (default: `@sweidos/eidos`) |
|
|
383
616
|
|
|
384
617
|
---
|
|
385
618
|
|
|
@@ -396,16 +629,51 @@ import { EidosDevtools } from '@sweidos/eidos/devtools';
|
|
|
396
629
|
|
|
397
630
|
Panel shows: live queue state · cache entries · SW status · offline simulation toggle.
|
|
398
631
|
|
|
632
|
+
### `eidosDebug()`
|
|
633
|
+
|
|
634
|
+
Returns a plain-object snapshot of the full Eidos runtime state — safe to `JSON.stringify`, useful for bug reports or attaching to error-tracking breadcrumbs:
|
|
635
|
+
|
|
636
|
+
```ts
|
|
637
|
+
import { eidosDebug } from '@sweidos/eidos';
|
|
638
|
+
|
|
639
|
+
// Print for a bug report
|
|
640
|
+
console.log(JSON.stringify(eidosDebug(), null, 2));
|
|
641
|
+
|
|
642
|
+
// Attach to a Sentry breadcrumb
|
|
643
|
+
Sentry.addBreadcrumb({ data: eidosDebug() });
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Snapshot includes: `version`, `swStatus`, `isOnline`, `resourceCount`, `resources` (per-URL status/hits/cachedAt), `queue` (item list with idempotencyKey/retryCount), `reliability` counters, and `swRegistration` state.
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
## Troubleshooting
|
|
651
|
+
|
|
652
|
+
Eidos emits plain-English `console.warn` messages in development (`import.meta.env.DEV`) for common setup problems:
|
|
653
|
+
|
|
654
|
+
| Warning | Cause | Fix |
|
|
655
|
+
| --------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
656
|
+
| `Service workers require a secure context` | `initEidos()` called on HTTP (non-localhost) | Use `localhost` for dev or deploy to HTTPS |
|
|
657
|
+
| `Service worker file not found at "/eidos-sw.js"` | SW file missing from `public/` | Add `eidos()` to `vite.config.ts` plugins, or copy `node_modules/@sweidos/eidos/dist/eidos-sw.js → public/eidos-sw.js` manually |
|
|
658
|
+
| `Service worker registration failed: …` | Unexpected registration error | Check `eidosDebug().swError` for the full browser error message |
|
|
659
|
+
| `Service workers are not supported in this context` | Old browser, or SW API absent | No fix needed — Eidos degrades gracefully; only SW-side caching is disabled |
|
|
660
|
+
|
|
399
661
|
---
|
|
400
662
|
|
|
401
663
|
## SSR adapters
|
|
402
664
|
|
|
403
665
|
**Next.js** — import from `@sweidos/eidos/nextjs`. Pre-marked `'use client'`, works in App Router layouts without a wrapper.
|
|
404
666
|
|
|
667
|
+
**Next.js Server Actions** — `@sweidos/next`'s `serverAction()` wraps a `'use server'` function with `action()` (`reliability: 'neverLose'` by default), keyed by `config.name` + `config.namespace`. `getActionContext()` / `idempotencyHeaders()` recover the `idempotencyKey`/`attempt` inside the action body.
|
|
668
|
+
|
|
405
669
|
**SvelteKit** — `initEidosSvelteKit()` inside `onMount`. Framework-agnostic stores (`$eidosQueue`, `$eidosStatus`) work with Svelte's `$` auto-subscribe.
|
|
406
670
|
|
|
407
671
|
**React Native** — `@sweidos/eidos/react-native` with AsyncStorage-backed queue. Same `action()` API surface, no Service Worker dependency.
|
|
408
672
|
|
|
673
|
+
**Tauri / Electron** — `@sweidos/sqlite-storage` with a SQLite-backed `QueueStorage`. Pass a `@tauri-apps/plugin-sql` `Database` directly, or wrap `better-sqlite3` with the `SqliteLike` interface. Same `action()` API surface, no Service Worker dependency.
|
|
674
|
+
|
|
675
|
+
**CRDT merge (Yjs)** — `@sweidos/crdt-yjs`'s `createYjsMergeResolver()` builds a `conflict.resolve` for the `'merge'`/`'custom'` strategy that applies the server's Yjs state and the queued local update to a `Y.Doc`, then rewrites the queued args with the merged update — automatic, loss-free reconciliation of concurrent edits instead of a hand-written `resolve()`.
|
|
676
|
+
|
|
409
677
|
---
|
|
410
678
|
|
|
411
679
|
## Known limitations
|
|
@@ -428,7 +696,7 @@ Panel shows: live queue state · cache entries · SW status · offline simulatio
|
|
|
428
696
|
| Offline writes | IndexedDB queue, auto-replay + backoff via `action()` | Background Sync, you wire it | No built-in mutation queue |
|
|
429
697
|
| Framework support | React, Svelte, Vue, Next.js, React Native, vanilla JS | Framework-agnostic (SW only) | Per-library |
|
|
430
698
|
| TanStack Query bridge | `@sweidos/eidos/query` adapter | — | Native |
|
|
431
|
-
| Bundle size (core) | ~6.
|
|
699
|
+
| Bundle size (core) | ~6.7 kB brotli | ~3-6 kB (modular) | ~13 kB |
|
|
432
700
|
|
|
433
701
|
Not a TanStack Query replacement — `@sweidos/eidos/query` is a thin adapter so
|
|
434
702
|
you keep TQ's cache/devtools while Eidos owns the offline layer. Workbox is a
|
package/dist/action.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ActionConfig, ActionHandle, ActionFn, ReplayResult } from './types';
|
|
2
|
+
export declare function action<TArgs extends any[], TReturn>(fn: ActionFn<TArgs, TReturn>, config: ActionConfig<TArgs>): ActionHandle<TArgs, TReturn>;
|
|
3
|
+
/**
|
|
4
|
+
* Cancel an invocation by its `idempotencyKey` (from `ActionContext` /
|
|
5
|
+
* `onOptimistic`). Aborts the in-flight call if `cancellable: true` and
|
|
6
|
+
* still running, otherwise removes a not-yet-replayed queued item.
|
|
7
|
+
* Returns `true` if something was cancelled/removed.
|
|
8
|
+
*
|
|
9
|
+
* Shared by every `ActionHandle.cancel()` and by devtools, which can't
|
|
10
|
+
* address a specific handle from a queue item alone.
|
|
11
|
+
*/
|
|
12
|
+
export declare function cancelByIdempotencyKey(idempotencyKey: string): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Reset a `'failed'` queue item back to `'pending'` so the next
|
|
15
|
+
* `replayQueue()` retries it — clears `error`/`nextRetryAt` and resets
|
|
16
|
+
* `retryCount` to 0. Returns `true` if the item existed and was failed.
|
|
17
|
+
* Used by devtools' per-item "Retry" action.
|
|
18
|
+
*/
|
|
19
|
+
export declare function requeueItem(id: string): Promise<boolean>;
|
|
20
|
+
export declare function replayQueue(): Promise<ReplayResult>;
|
|
21
|
+
/** Remove all items from the action queue (storage + in-memory store). */
|
|
22
|
+
export declare function clearQueue(): Promise<void>;
|