@sweidos/eidos 2.2.0 → 2.3.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.
- package/README.md +261 -15
- package/dist/action.d.ts +22 -0
- 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 +185 -5
- package/dist/eidos-sw.js +62 -20
- package/dist/eidos.cjs +6 -6
- package/dist/eidos.cjs.map +1 -1
- package/dist/idb.d.ts +10 -0
- package/dist/index.d.ts +20 -647
- package/dist/index.js +37 -34
- 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/replay.d.ts +15 -0
- package/dist/resource.d.ts +32 -0
- package/dist/resource.js +81 -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 +24 -21
- package/dist/runtime.js.map +1 -1
- package/dist/store-slices.d.ts +26 -0
- package/dist/store.d.ts +15 -0
- package/dist/stores.d.ts +64 -0
- 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.d.ts +1 -2
- package/dist/types.d.ts +311 -0
- 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 +11 -7
package/README.md
CHANGED
|
@@ -139,15 +139,15 @@ if ('queued' in result) {
|
|
|
139
139
|
| -------------------------- | ----------------------------- | -------------------------------------------------------------- |
|
|
140
140
|
| **React** | `@sweidos/eidos` | Hooks + `EidosProvider` |
|
|
141
141
|
| **Next.js App Router** | `@sweidos/eidos/nextjs` | Pre-marked `'use client'` — no wrapper needed |
|
|
142
|
-
| **Next.js Server Actions** | `@
|
|
142
|
+
| **Next.js Server Actions** | `@sweidos/next` | `serverAction()` neverLose wrapper + idempotency context |
|
|
143
143
|
| **SvelteKit** | `@sweidos/eidos/sveltekit` | `initEidosSvelteKit()` in `onMount`, framework-agnostic stores |
|
|
144
144
|
| **Vue** | `@sweidos/eidos` | Framework-agnostic stores via `eidosStatus.subscribe()` |
|
|
145
145
|
| **React Native** | `@sweidos/eidos/react-native` | AsyncStorage-backed queue, same `action()` API |
|
|
146
146
|
| **Vanilla JS** | `@sweidos/eidos` | `eidosStatus`, `eidosQueue`, `eidosQueueStats` stores |
|
|
147
147
|
| **Vite** | `@sweidos/eidos/vite` | Plugin auto-copies `eidos-sw.js` on every build |
|
|
148
|
-
| **CRDT merge (Yjs)** | `@
|
|
148
|
+
| **CRDT merge (Yjs)** | `@sweidos/crdt-yjs` | `createYjsMergeResolver()` for `conflict.strategy: 'merge'` |
|
|
149
149
|
| **TanStack Query** | `@sweidos/eidos/query` | `useEidosQuery`, `useEidosMutation`, `withEidosQueryClient` |
|
|
150
|
-
| **Tauri / Electron** | `@
|
|
150
|
+
| **Tauri / Electron** | `@sweidos/sqlite-storage` | SQLite-backed `QueueStorage`, same `action()` API |
|
|
151
151
|
|
|
152
152
|
---
|
|
153
153
|
|
|
@@ -162,10 +162,15 @@ const products = resource('/api/products', {
|
|
|
162
162
|
offline: true, // enable SW interception + caching
|
|
163
163
|
strategy?: 'cache-first' | 'stale-while-revalidate' | 'network-first',
|
|
164
164
|
cacheName?: string, // custom cache bucket
|
|
165
|
-
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
|
|
167
|
+
networkTimeoutMs?: number, // ms before falling back to cache (network-first & SWR). Default: 3000
|
|
166
168
|
version?: string | number, // bump when the response shape changes —
|
|
167
169
|
// appended to cacheName (e.g. 'eidos-resources-v1-v2')
|
|
168
|
-
// so old-shaped cache entries aren't served
|
|
170
|
+
// so old-shaped cache entries aren't served.
|
|
171
|
+
// NOTE: this is separate from the SW-internal CACHE_VERSION
|
|
172
|
+
// (bumped only on Eidos releases to purge old cache buckets).
|
|
173
|
+
// Bump `version` for your data shape; Eidos bumps CACHE_VERSION.
|
|
169
174
|
})
|
|
170
175
|
|
|
171
176
|
await products.fetch() // Promise<Response>
|
|
@@ -197,6 +202,19 @@ await productPattern.invalidate(); // clear all cached entries matching the patt
|
|
|
197
202
|
productPattern.unregister();
|
|
198
203
|
```
|
|
199
204
|
|
|
205
|
+
| Token | Matches |
|
|
206
|
+
| -------- | ------------------------------------------------------------ |
|
|
207
|
+
| `*` | One path segment — `/api/products/*` ↔ `/api/products/4` |
|
|
208
|
+
| `**` | Any number of segments — `https://cdn.example.com/assets/**` |
|
|
209
|
+
| `:param` | A named segment — `/api/users/:id/orders` |
|
|
210
|
+
|
|
211
|
+
Pass the full URL (including origin) for cross-origin resources, e.g.
|
|
212
|
+
`resourcePattern('https://cdn.example.com/assets/**', { offline: true })`.
|
|
213
|
+
|
|
214
|
+
See it live in the [playground docs → Examples → URL patterns, one
|
|
215
|
+
registration per
|
|
216
|
+
family](https://sweidos.vercel.app/docs/examples#url-patterns-one-registration-per-family).
|
|
217
|
+
|
|
200
218
|
### `action(fn, config)`
|
|
201
219
|
|
|
202
220
|
```ts
|
|
@@ -226,6 +244,62 @@ await cancelByIdempotencyKey(idempotencyKey) // true if cancelled/removed
|
|
|
226
244
|
await requeueItem(queueItemId) // true if it was 'failed'
|
|
227
245
|
```
|
|
228
246
|
|
|
247
|
+
### Conflict resolution
|
|
248
|
+
|
|
249
|
+
A `neverLose` action can sit in the queue for a while — by the time it
|
|
250
|
+
replays, the world may have moved on (the requested stock sold out, the
|
|
251
|
+
record was deleted, etc.). `conflict` decides what happens when a replay gets
|
|
252
|
+
a 4xx response, instead of retrying forever or silently dropping the write:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
class StockConflictError extends Error {
|
|
256
|
+
status = 409;
|
|
257
|
+
constructor(public available: number) {
|
|
258
|
+
super('insufficient stock');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export const reserveStock = action(
|
|
263
|
+
async (payload: { productId: number; quantity: number }) => {
|
|
264
|
+
const res = await fetch('/api/inventory', {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
body: JSON.stringify(payload),
|
|
267
|
+
});
|
|
268
|
+
if (res.status === 409) {
|
|
269
|
+
const { available } = await res.json();
|
|
270
|
+
throw new StockConflictError(available);
|
|
271
|
+
}
|
|
272
|
+
if (!res.ok) throw new Error('Reservation failed');
|
|
273
|
+
return res.json();
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
reliability: 'neverLose',
|
|
277
|
+
name: 'reserveStock',
|
|
278
|
+
conflict: {
|
|
279
|
+
strategy: 'custom',
|
|
280
|
+
resolve: ({ error, args, attempt }) => {
|
|
281
|
+
if (error instanceof StockConflictError && error.available > 0) {
|
|
282
|
+
const [payload] = args;
|
|
283
|
+
// Rewrite the queued args and retry with what's actually available
|
|
284
|
+
return { resolved: [{ ...payload, quantity: error.available }] };
|
|
285
|
+
}
|
|
286
|
+
return 'skip'; // nothing left to reserve — drop the write
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
| Strategy | Behavior on 4xx replay |
|
|
294
|
+
| ------------ | --------------------------------------------------------------------- |
|
|
295
|
+
| `serverWins` | Drop the queued item — the server's current state is authoritative. |
|
|
296
|
+
| `clientWins` | Keep retrying — the write should eventually succeed. |
|
|
297
|
+
| `merge` | Call `resolve(ctx)`; typically used to combine client + server state. |
|
|
298
|
+
| `custom` | Call `resolve(ctx)`; return `'retry'`, `'skip'`, or `{ resolved }`. |
|
|
299
|
+
|
|
300
|
+
See it live in the [playground docs → Examples → Conflict resolution on
|
|
301
|
+
replay](https://sweidos.vercel.app/docs/examples#conflict-resolution-on-replay).
|
|
302
|
+
|
|
229
303
|
### React hooks
|
|
230
304
|
|
|
231
305
|
```ts
|
|
@@ -266,6 +340,86 @@ initEidos({
|
|
|
266
340
|
The same counters are visible live in `<EidosDevtools />` under the
|
|
267
341
|
"Reliability" tab.
|
|
268
342
|
|
|
343
|
+
### Handling SW updates
|
|
344
|
+
|
|
345
|
+
By default, when a new service worker is available it activates immediately
|
|
346
|
+
(`skipWaiting: true`). This matches standard PWA behaviour but can interrupt
|
|
347
|
+
in-flight requests on pages that are mid-navigation.
|
|
348
|
+
|
|
349
|
+
Opt into the **toast-then-reload** pattern with `skipWaiting: false`:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { initEidos, triggerSwUpdate } from '@sweidos/eidos';
|
|
353
|
+
|
|
354
|
+
initEidos({
|
|
355
|
+
skipWaiting: false,
|
|
356
|
+
onUpdateAvailable: (_registration) => {
|
|
357
|
+
// Show a toast, banner, or dialog — then call triggerSwUpdate() when the
|
|
358
|
+
// user confirms they're ready to reload.
|
|
359
|
+
showToast({
|
|
360
|
+
message: 'App update ready',
|
|
361
|
+
action: { label: 'Reload', onClick: triggerSwUpdate },
|
|
362
|
+
});
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
`triggerSwUpdate()` tells the waiting service worker to activate, then the
|
|
368
|
+
browser reloads the page. With `skipWaiting: true` (default) `onUpdateAvailable`
|
|
369
|
+
is never called and `triggerSwUpdate()` is not needed.
|
|
370
|
+
|
|
371
|
+
> **Tip**: avoid calling `triggerSwUpdate()` while `neverLose` actions are
|
|
372
|
+
> mid-replay. The replay coordination (BroadcastChannel + Web Locks) survives
|
|
373
|
+
> SW activation, but triggering an update during an active replay pass adds
|
|
374
|
+
> unnecessary churn. Wait until the queue drains or use `waitForQueueDrain()`
|
|
375
|
+
> from `@sweidos/eidos/testing` in tests.
|
|
376
|
+
|
|
377
|
+
### Queue management
|
|
378
|
+
|
|
379
|
+
Inspect and manage the offline action queue directly — handy for "pending
|
|
380
|
+
changes" panels, manual retry buttons, or a "discard my offline edits" action:
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
import {
|
|
384
|
+
useEidosQueue,
|
|
385
|
+
cancelByIdempotencyKey,
|
|
386
|
+
requeueItem,
|
|
387
|
+
clearQueue,
|
|
388
|
+
replayQueue,
|
|
389
|
+
} from '@sweidos/eidos';
|
|
390
|
+
|
|
391
|
+
function QueuePanel() {
|
|
392
|
+
const queue = useEidosQueue(); // live list of pending/replaying/failed items
|
|
393
|
+
|
|
394
|
+
return queue.map((item) => (
|
|
395
|
+
<li key={item.id}>
|
|
396
|
+
{item.actionName} — {item.status}
|
|
397
|
+
|
|
398
|
+
{/* Drop a write before it ever reaches the server */}
|
|
399
|
+
{item.status === 'pending' && (
|
|
400
|
+
<button onClick={() => cancelByIdempotencyKey(item.idempotencyKey)}>
|
|
401
|
+
Cancel
|
|
402
|
+
</button>
|
|
403
|
+
)}
|
|
404
|
+
|
|
405
|
+
{/* Reset a failed item to 'pending' and replay it */}
|
|
406
|
+
{item.status === 'failed' && (
|
|
407
|
+
<button onClick={() => requeueItem(item.id)}>Retry</button>
|
|
408
|
+
)}
|
|
409
|
+
</li>
|
|
410
|
+
));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Drop every queued write — e.g. on "discard offline changes"
|
|
414
|
+
await clearQueue();
|
|
415
|
+
|
|
416
|
+
// Force a replay pass — normally triggered automatically on reconnect
|
|
417
|
+
await replayQueue();
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
See it live in the [playground docs → Examples → Queue management &
|
|
421
|
+
reliability stats](https://sweidos.vercel.app/docs/examples#queue-management-reliability-stats).
|
|
422
|
+
|
|
269
423
|
---
|
|
270
424
|
|
|
271
425
|
## TanStack Query
|
|
@@ -365,6 +519,10 @@ for client-side routing; otherwise the SW opens `data.url` directly.
|
|
|
365
519
|
|
|
366
520
|
## Testing
|
|
367
521
|
|
|
522
|
+
`@sweidos/eidos/testing` runs entirely at the JS layer — no real Service
|
|
523
|
+
Worker needed — and gives Vitest/Jest/Playwright direct control over online
|
|
524
|
+
state, the action queue, and the resource cache.
|
|
525
|
+
|
|
368
526
|
```ts
|
|
369
527
|
import {
|
|
370
528
|
mockOffline,
|
|
@@ -380,29 +538,82 @@ import {
|
|
|
380
538
|
beforeEach(() => resetEidos());
|
|
381
539
|
|
|
382
540
|
it('queues action while offline', async () => {
|
|
383
|
-
mockOffline();
|
|
384
|
-
await createOrder({ productId: 1,
|
|
541
|
+
mockOffline({ stubFetch: true });
|
|
542
|
+
await createOrder({ productId: 1, quantity: 2 });
|
|
385
543
|
expect(getEidosState().queue).toHaveLength(1);
|
|
544
|
+
expect(getEidosState().queue[0].actionName).toBe('createOrder');
|
|
386
545
|
});
|
|
387
546
|
|
|
388
547
|
it('replays on reconnect', async () => {
|
|
389
548
|
mockOffline();
|
|
390
|
-
await createOrder({ productId: 1,
|
|
549
|
+
await createOrder({ productId: 1, quantity: 2 });
|
|
550
|
+
|
|
551
|
+
// Forces isOnline = true and replays immediately
|
|
391
552
|
const result = await drainQueue();
|
|
392
553
|
expect(result.succeeded).toBe(1);
|
|
554
|
+
|
|
555
|
+
// ...or for code that replays itself on the 'online' event:
|
|
556
|
+
await waitForQueueDrain({ timeout: 2000 });
|
|
557
|
+
expect(getEidosState().queue).toHaveLength(0);
|
|
393
558
|
});
|
|
559
|
+
|
|
560
|
+
it('caches GET responses for offline use', async () => {
|
|
561
|
+
await products.json();
|
|
562
|
+
const cached = await getCachedEntry('/api/products');
|
|
563
|
+
expect(cached).toBeDefined();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
afterEach(() => clearEidosCache());
|
|
394
567
|
```
|
|
395
568
|
|
|
396
569
|
---
|
|
397
570
|
|
|
398
571
|
## OpenAPI codegen
|
|
399
572
|
|
|
573
|
+
`eidos-gen` reads an OpenAPI 3.x spec (JSON or YAML) and writes typed
|
|
574
|
+
`resource()` + `action()` declarations — request/response interfaces from
|
|
575
|
+
`$ref` schemas, `{id}` → `:id` path-param conversion, and DELETE body
|
|
576
|
+
omission.
|
|
577
|
+
|
|
400
578
|
```bash
|
|
401
|
-
npx eidos-gen openapi.json
|
|
402
|
-
|
|
579
|
+
npx eidos-gen openapi.json --out src/lib/eidos.generated.ts
|
|
580
|
+
|
|
581
|
+
eidos-gen: reading openapi.json
|
|
582
|
+
eidos-gen: wrote src/lib/eidos.generated.ts
|
|
583
|
+
2 resource(s), 2 action(s)
|
|
584
|
+
2 type(s)
|
|
403
585
|
```
|
|
404
586
|
|
|
405
|
-
|
|
587
|
+
```ts
|
|
588
|
+
// eidos.generated.ts
|
|
589
|
+
import { resource, action } from '@sweidos/eidos';
|
|
590
|
+
|
|
591
|
+
export interface Product {
|
|
592
|
+
id: number;
|
|
593
|
+
name: string;
|
|
594
|
+
tags?: string[];
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export const listProducts = resource('/products', { offline: true });
|
|
598
|
+
|
|
599
|
+
export const createProduct = action(
|
|
600
|
+
async (payload: Product): Promise<Product> => {
|
|
601
|
+
const res = await fetch('/products', {
|
|
602
|
+
method: 'POST',
|
|
603
|
+
headers: { 'Content-Type': 'application/json' },
|
|
604
|
+
body: JSON.stringify(payload),
|
|
605
|
+
});
|
|
606
|
+
return res.json();
|
|
607
|
+
},
|
|
608
|
+
{ reliability: 'neverLose', name: 'createProduct' },
|
|
609
|
+
);
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
| Flag | Effect |
|
|
613
|
+
| -------------- | ------------------------------------------------------------ |
|
|
614
|
+
| `--out, -o` | Output file path (default: `eidos.generated.ts`) |
|
|
615
|
+
| `--no-offline` | Set `offline: false` on every generated `resource()` |
|
|
616
|
+
| `--eidos` | Import path for `@sweidos/eidos` (default: `@sweidos/eidos`) |
|
|
406
617
|
|
|
407
618
|
---
|
|
408
619
|
|
|
@@ -419,21 +630,56 @@ import { EidosDevtools } from '@sweidos/eidos/devtools';
|
|
|
419
630
|
|
|
420
631
|
Panel shows: live queue state · cache entries · SW status · offline simulation toggle.
|
|
421
632
|
|
|
633
|
+
### `eidosDebug()`
|
|
634
|
+
|
|
635
|
+
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:
|
|
636
|
+
|
|
637
|
+
```ts
|
|
638
|
+
import { eidosDebug } from '@sweidos/eidos';
|
|
639
|
+
|
|
640
|
+
// Print for a bug report
|
|
641
|
+
console.log(JSON.stringify(eidosDebug(), null, 2));
|
|
642
|
+
|
|
643
|
+
// Attach to a Sentry breadcrumb
|
|
644
|
+
Sentry.addBreadcrumb({ data: eidosDebug() });
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
Snapshot includes: `version`, `swStatus`, `isOnline`, `resourceCount`, `resources` (per-URL status/hits/cachedAt), `queue` (item list with idempotencyKey/retryCount), `reliability` counters, and `swRegistration` state.
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## Troubleshooting
|
|
652
|
+
|
|
653
|
+
Eidos emits plain-English `console.warn` messages in development (`import.meta.env.DEV`) for common setup problems:
|
|
654
|
+
|
|
655
|
+
| Warning | Cause | Fix |
|
|
656
|
+
| --------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
657
|
+
| `Service workers require a secure context` | `initEidos()` called on HTTP (non-localhost) | Use `localhost` for dev or deploy to HTTPS |
|
|
658
|
+
| `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 |
|
|
659
|
+
| `Service worker registration failed: …` | Unexpected registration error | Check `eidosDebug().swError` for the full browser error message |
|
|
660
|
+
| `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 |
|
|
661
|
+
|
|
662
|
+
**[→ Getting started guide](docs/guides/getting-started.md)** — zero-jargon walkthrough: install → Vite plugin → wrap app → first resource + action → offline status UI.
|
|
663
|
+
|
|
664
|
+
**[→ Full troubleshooting guide](docs/guides/troubleshooting.md)** — per-warning copy-pasteable fixes, runtime issues (stuck SW, maxAge, networkTimeoutMs), and `eidosDebug()` field reference.
|
|
665
|
+
|
|
666
|
+
**[→ Glossary](docs/guides/glossary.md)** — plain-language definitions of service worker, cache strategy, idempotency key, replay queue, and more.
|
|
667
|
+
|
|
422
668
|
---
|
|
423
669
|
|
|
424
670
|
## SSR adapters
|
|
425
671
|
|
|
426
672
|
**Next.js** — import from `@sweidos/eidos/nextjs`. Pre-marked `'use client'`, works in App Router layouts without a wrapper.
|
|
427
673
|
|
|
428
|
-
**Next.js Server Actions** — `@
|
|
674
|
+
**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.
|
|
429
675
|
|
|
430
676
|
**SvelteKit** — `initEidosSvelteKit()` inside `onMount`. Framework-agnostic stores (`$eidosQueue`, `$eidosStatus`) work with Svelte's `$` auto-subscribe.
|
|
431
677
|
|
|
432
678
|
**React Native** — `@sweidos/eidos/react-native` with AsyncStorage-backed queue. Same `action()` API surface, no Service Worker dependency.
|
|
433
679
|
|
|
434
|
-
**Tauri / Electron** — `@
|
|
680
|
+
**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.
|
|
435
681
|
|
|
436
|
-
**CRDT merge (Yjs)** — `@
|
|
682
|
+
**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()`.
|
|
437
683
|
|
|
438
684
|
---
|
|
439
685
|
|
|
@@ -457,7 +703,7 @@ Panel shows: live queue state · cache entries · SW status · offline simulatio
|
|
|
457
703
|
| Offline writes | IndexedDB queue, auto-replay + backoff via `action()` | Background Sync, you wire it | No built-in mutation queue |
|
|
458
704
|
| Framework support | React, Svelte, Vue, Next.js, React Native, vanilla JS | Framework-agnostic (SW only) | Per-library |
|
|
459
705
|
| TanStack Query bridge | `@sweidos/eidos/query` adapter | — | Native |
|
|
460
|
-
| Bundle size (core) | ~6.
|
|
706
|
+
| Bundle size (core) | ~6.7 kB brotli | ~3-6 kB (modular) | ~13 kB |
|
|
461
707
|
|
|
462
708
|
Not a TanStack Query replacement — `@sweidos/eidos/query` is a thin adapter so
|
|
463
709
|
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>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ActionQueueItem } from './types';
|
|
2
|
+
import { QueueStorage } from './queue-storage';
|
|
3
|
+
/** Minimal subset of @react-native-async-storage/async-storage (or any compatible key-value store). */
|
|
4
|
+
export interface AsyncStorageLike {
|
|
5
|
+
getItem(key: string): Promise<string | null>;
|
|
6
|
+
setItem(key: string, value: string): Promise<void>;
|
|
7
|
+
removeItem(key: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* QueueStorage implementation backed by any AsyncStorage-compatible API.
|
|
11
|
+
* Pass the AsyncStorage singleton from @react-native-async-storage/async-storage
|
|
12
|
+
* (or MMKV, SQLite, or any store that satisfies AsyncStorageLike).
|
|
13
|
+
*/
|
|
14
|
+
export declare class AsyncStorageQueueStorage implements QueueStorage {
|
|
15
|
+
private readonly storage;
|
|
16
|
+
constructor(storage: AsyncStorageLike);
|
|
17
|
+
private readAll;
|
|
18
|
+
private writeAll;
|
|
19
|
+
add(item: ActionQueueItem): Promise<void>;
|
|
20
|
+
getAll(): Promise<ActionQueueItem[]>;
|
|
21
|
+
getPending(): Promise<ActionQueueItem[]>;
|
|
22
|
+
update(id: string, patch: Partial<ActionQueueItem>): Promise<void>;
|
|
23
|
+
remove(id: string): Promise<void>;
|
|
24
|
+
clear(): Promise<void>;
|
|
25
|
+
}
|
package/dist/debug.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ReliabilityStats } from './types';
|
|
2
|
+
export interface EidosDebugSnapshot {
|
|
3
|
+
version: string;
|
|
4
|
+
swStatus: string;
|
|
5
|
+
swError?: string;
|
|
6
|
+
isOnline: boolean;
|
|
7
|
+
resourceCount: number;
|
|
8
|
+
resources: Record<string, {
|
|
9
|
+
url: string;
|
|
10
|
+
strategy: string;
|
|
11
|
+
status: string;
|
|
12
|
+
cacheHits: number;
|
|
13
|
+
cacheMisses: number;
|
|
14
|
+
cachedAt?: number;
|
|
15
|
+
}>;
|
|
16
|
+
queue: {
|
|
17
|
+
id: string;
|
|
18
|
+
actionId: string;
|
|
19
|
+
actionName: string;
|
|
20
|
+
status: string;
|
|
21
|
+
retryCount: number;
|
|
22
|
+
maxRetries: number;
|
|
23
|
+
idempotencyKey: string;
|
|
24
|
+
schemaVersion: number;
|
|
25
|
+
queuedAt: number;
|
|
26
|
+
}[];
|
|
27
|
+
reliability: ReliabilityStats;
|
|
28
|
+
swRegistration: {
|
|
29
|
+
scope: string;
|
|
30
|
+
scriptURL: string;
|
|
31
|
+
state: 'installing' | 'waiting' | 'active' | null;
|
|
32
|
+
} | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Returns a plain-object snapshot of the current Eidos runtime state.
|
|
36
|
+
* Safe to serialize with `JSON.stringify`. Useful for bug reports,
|
|
37
|
+
* attaching to error tracking breadcrumbs, and the devtools SW tab.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Print for a bug report:
|
|
41
|
+
* console.log(JSON.stringify(eidosDebug(), null, 2));
|
|
42
|
+
*
|
|
43
|
+
* // Attach to a Sentry breadcrumb:
|
|
44
|
+
* Sentry.addBreadcrumb({ data: eidosDebug() });
|
|
45
|
+
*/
|
|
46
|
+
export declare function eidosDebug(): EidosDebugSnapshot;
|
package/dist/debug.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useEidosStore as a } from "./store.js";
|
|
2
|
+
import { getSwRegistration as r } from "./sw-bridge.js";
|
|
3
|
+
import { VERSION as c } from "./version.js";
|
|
4
|
+
function l() {
|
|
5
|
+
const t = a.getState(), s = r();
|
|
6
|
+
return {
|
|
7
|
+
version: c,
|
|
8
|
+
swStatus: t.swStatus,
|
|
9
|
+
...t.swError !== void 0 && { swError: t.swError },
|
|
10
|
+
isOnline: t.isOnline,
|
|
11
|
+
resourceCount: Object.keys(t.resources).length,
|
|
12
|
+
resources: Object.fromEntries(Object.entries(t.resources).map(([e, i]) => [e, {
|
|
13
|
+
url: i.url,
|
|
14
|
+
strategy: i.strategy.swStrategy,
|
|
15
|
+
status: i.status,
|
|
16
|
+
cacheHits: i.cacheHits,
|
|
17
|
+
cacheMisses: i.cacheMisses,
|
|
18
|
+
...i.cachedAt !== void 0 && { cachedAt: i.cachedAt }
|
|
19
|
+
}])),
|
|
20
|
+
queue: t.queue.map((e) => ({
|
|
21
|
+
id: e.id,
|
|
22
|
+
actionId: e.actionId,
|
|
23
|
+
actionName: e.actionName,
|
|
24
|
+
status: e.status,
|
|
25
|
+
retryCount: e.retryCount,
|
|
26
|
+
maxRetries: e.maxRetries,
|
|
27
|
+
idempotencyKey: e.idempotencyKey,
|
|
28
|
+
schemaVersion: e.schemaVersion,
|
|
29
|
+
queuedAt: e.queuedAt
|
|
30
|
+
})),
|
|
31
|
+
reliability: { ...t.reliability },
|
|
32
|
+
swRegistration: s ? {
|
|
33
|
+
scope: s.scope,
|
|
34
|
+
scriptURL: (s.active ?? s.waiting ?? s.installing)?.scriptURL ?? "",
|
|
35
|
+
state: s.installing ? "installing" : s.waiting ? "waiting" : s.active ? "active" : null
|
|
36
|
+
} : null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
l as eidosDebug
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=debug.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debug.js","names":[],"sources":["../src/debug.ts"],"sourcesContent":["import { useEidosStore } from './store';\nimport { getSwRegistration } from './sw-bridge';\nimport { VERSION } from './version';\nimport type { ReliabilityStats } from './types';\n\nexport interface EidosDebugSnapshot {\n version: string;\n swStatus: string;\n swError?: string;\n isOnline: boolean;\n resourceCount: number;\n resources: Record<\n string,\n {\n url: string;\n strategy: string;\n status: string;\n cacheHits: number;\n cacheMisses: number;\n cachedAt?: number;\n }\n >;\n queue: {\n id: string;\n actionId: string;\n actionName: string;\n status: string;\n retryCount: number;\n maxRetries: number;\n idempotencyKey: string;\n schemaVersion: number;\n queuedAt: number;\n }[];\n reliability: ReliabilityStats;\n swRegistration: {\n scope: string;\n scriptURL: string;\n state: 'installing' | 'waiting' | 'active' | null;\n } | null;\n}\n\n/**\n * Returns a plain-object snapshot of the current Eidos runtime state.\n * Safe to serialize with `JSON.stringify`. Useful for bug reports,\n * attaching to error tracking breadcrumbs, and the devtools SW tab.\n *\n * @example\n * // Print for a bug report:\n * console.log(JSON.stringify(eidosDebug(), null, 2));\n *\n * // Attach to a Sentry breadcrumb:\n * Sentry.addBreadcrumb({ data: eidosDebug() });\n */\nexport function eidosDebug(): EidosDebugSnapshot {\n const state = useEidosStore.getState();\n const swReg = getSwRegistration();\n\n return {\n version: VERSION,\n swStatus: state.swStatus,\n ...(state.swError !== undefined && { swError: state.swError }),\n isOnline: state.isOnline,\n resourceCount: Object.keys(state.resources).length,\n resources: Object.fromEntries(\n Object.entries(state.resources).map(([url, entry]) => [\n url,\n {\n url: entry.url,\n strategy: entry.strategy.swStrategy,\n status: entry.status,\n cacheHits: entry.cacheHits,\n cacheMisses: entry.cacheMisses,\n ...(entry.cachedAt !== undefined && { cachedAt: entry.cachedAt }),\n },\n ]),\n ),\n queue: state.queue.map((item) => ({\n id: item.id,\n actionId: item.actionId,\n actionName: item.actionName,\n status: item.status,\n retryCount: item.retryCount,\n maxRetries: item.maxRetries,\n idempotencyKey: item.idempotencyKey,\n schemaVersion: item.schemaVersion,\n queuedAt: item.queuedAt,\n })),\n reliability: { ...state.reliability },\n swRegistration: swReg\n ? {\n scope: swReg.scope,\n scriptURL: (swReg.active ?? swReg.waiting ?? swReg.installing)?.scriptURL ?? '',\n state: swReg.installing\n ? 'installing'\n : swReg.waiting\n ? 'waiting'\n : swReg.active\n ? 'active'\n : null,\n }\n : null,\n };\n}\n"],"mappings":";;;AAqDA,SAAgB,IAAiC;AAC/C,QAAM,IAAQ,EAAc,SAAS,GAC/B,IAAQ,EAAkB;AAEhC,SAAO;AAAA,IACL,SAAS;AAAA,IACT,UAAU,EAAM;AAAA,IAChB,GAAI,EAAM,YAAY,UAAa,EAAE,SAAS,EAAM,QAAQ;AAAA,IAC5D,UAAU,EAAM;AAAA,IAChB,eAAe,OAAO,KAAK,EAAM,SAAS,EAAE;AAAA,IAC5C,WAAW,OAAO,YAChB,OAAO,QAAQ,EAAM,SAAS,EAAE,IAAA,CAAK,CAAC,GAAK,CAAA,MAAW,CACpD,GACA;AAAA,MACE,KAAK,EAAM;AAAA,MACX,UAAU,EAAM,SAAS;AAAA,MACzB,QAAQ,EAAM;AAAA,MACd,WAAW,EAAM;AAAA,MACjB,aAAa,EAAM;AAAA,MACnB,GAAI,EAAM,aAAa,UAAa,EAAE,UAAU,EAAM,SAAS;AAAA,IACjE,CACF,CAAC,CACH;AAAA,IACA,OAAO,EAAM,MAAM,IAAA,CAAK,OAAU;AAAA,MAChC,IAAI,EAAK;AAAA,MACT,UAAU,EAAK;AAAA,MACf,YAAY,EAAK;AAAA,MACjB,QAAQ,EAAK;AAAA,MACb,YAAY,EAAK;AAAA,MACjB,YAAY,EAAK;AAAA,MACjB,gBAAgB,EAAK;AAAA,MACrB,eAAe,EAAK;AAAA,MACpB,UAAU,EAAK;AAAA,IACjB,EAAE;AAAA,IACF,aAAa,EAAE,GAAG,EAAM,YAAY;AAAA,IACpC,gBAAgB,IACZ;AAAA,MACE,OAAO,EAAM;AAAA,MACb,YAAY,EAAM,UAAU,EAAM,WAAW,EAAM,aAAa,aAAa;AAAA,MAC7E,OAAO,EAAM,aACT,eACA,EAAM,UACJ,YACA,EAAM,SACJ,WACA;AAAA,IACV,IACA;AAAA,EACN;AACF"}
|