lowdata 0.1.0 → 0.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 (42) hide show
  1. package/README.md +37 -4
  2. package/dist/{client-CqO3Y1J5.d.cts → client-BmsR0Pf6.d.cts} +8 -2
  3. package/dist/{client-BvdWSYIM.d.ts → client-DSKoYUj9.d.ts} +8 -2
  4. package/dist/forms.cjs +29 -11
  5. package/dist/forms.cjs.map +1 -1
  6. package/dist/forms.d.cts +4 -4
  7. package/dist/forms.d.ts +4 -4
  8. package/dist/forms.js +29 -11
  9. package/dist/forms.js.map +1 -1
  10. package/dist/index.cjs +29 -11
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +4 -4
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +29 -11
  15. package/dist/index.js.map +1 -1
  16. package/dist/media.cjs +1 -1
  17. package/dist/media.cjs.map +1 -1
  18. package/dist/media.d.cts +2 -2
  19. package/dist/media.d.ts +2 -2
  20. package/dist/media.js +1 -1
  21. package/dist/media.js.map +1 -1
  22. package/dist/network.cjs +29 -11
  23. package/dist/network.cjs.map +1 -1
  24. package/dist/network.d.cts +7 -6
  25. package/dist/network.d.ts +7 -6
  26. package/dist/network.js +29 -11
  27. package/dist/network.js.map +1 -1
  28. package/dist/{progressiveImage-BhY_K8Dj.d.ts → progressiveImage-BOdic4dJ.d.ts} +1 -1
  29. package/dist/{progressiveImage-BWNdewbi.d.cts → progressiveImage-CDpz_rv3.d.cts} +1 -1
  30. package/dist/react.cjs +29 -11
  31. package/dist/react.cjs.map +1 -1
  32. package/dist/react.d.cts +4 -4
  33. package/dist/react.d.ts +4 -4
  34. package/dist/react.js +29 -11
  35. package/dist/react.js.map +1 -1
  36. package/dist/{retry-C3zL9T5Z.d.cts → retry-BnuESos0.d.cts} +1 -1
  37. package/dist/{retry-D6DfKGOi.d.ts → retry-CnvZhHan.d.ts} +1 -1
  38. package/dist/{types-Bn1BAcch.d.ts → types-1ha2HIfv.d.ts} +1 -1
  39. package/dist/{types-CZDYS-fB.d.cts → types-Cbm1nNDO.d.cts} +1 -1
  40. package/dist/{types--FRrBa-i.d.cts → types-D6fjl8Wy.d.cts} +22 -1
  41. package/dist/{types--FRrBa-i.d.ts → types-D6fjl8Wy.d.ts} +22 -1
  42. package/package.json +22 -2
package/README.md CHANGED
@@ -6,7 +6,9 @@ Automatic retries with backoff, an offline request queue that survives page relo
6
6
  forms, and bandwidth-aware image compression — for web apps that have to keep working on 2G, on a
7
7
  flaky café Wi-Fi, or mid-load-shedding. Framework-agnostic, near-zero dependencies, fully typed.
8
8
 
9
- [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
9
+ [![CI](https://github.com/bassmoses/lowdata/actions/workflows/ci.yml/badge.svg)](https://github.com/bassmoses/lowdata/actions/workflows/ci.yml)
10
+ [![npm version](https://img.shields.io/npm/v/lowdata.svg)](https://www.npmjs.com/package/lowdata)
11
+ [![license](https://img.shields.io/npm/l/lowdata.svg)](./LICENSE)
10
12
  [![bundle size](https://img.shields.io/badge/core%20%2B%20network%20%2B%20forms-~9%20KB%20gzip-brightgreen.svg)](#bundle-size)
11
13
 
12
14
  ```ts
@@ -138,6 +140,20 @@ client.onSync((event) => {
138
140
  });
139
141
  ```
140
142
 
143
+ Background sync failures are deliberately never thrown (a queued item retrying in the background
144
+ shouldn't crash your app) — but they're not silent either. Pass `onError` to see them (a
145
+ `console.warn` is the default if you don't):
146
+
147
+ ```ts
148
+ createLowdataClient({
149
+ onError: (error, { scope }) => reportToMonitoring(error, { scope }),
150
+ // scope: 'db-open' (IndexedDB unavailable, fell back to memory for the session)
151
+ // | 'db-operation' (one IndexedDB call failed — e.g. a transient quota error — persistence
152
+ // is still available, just that one call fell back)
153
+ // | 'sync' (the background sync loop hit an unexpected error)
154
+ });
155
+ ```
156
+
141
157
  ### Retry & backoff
142
158
 
143
159
  ```ts
@@ -228,8 +244,24 @@ cross-tab-raced request is still possible in rare edge cases.
228
244
  for that session.
229
245
  - **SSR-safe to import**: `createLowdataClient()` and friends never assume `window`/`navigator`
230
246
  exist; on the server, connection quality reports `'online'` and nothing touches the DOM.
231
- - Sync only runs while a tab is open (no Service Worker in v1) — closing the tab while offline
232
- defers sync to the next time the app is opened, not true background sync.
247
+
248
+ ## Known limitations & roadmap
249
+
250
+ - **Sync only runs while a tab is open.** There's no Service Worker in v1 (by design — see
251
+ "vs. alternatives" below) — closing the tab while offline defers sync to the next time the app
252
+ is opened, not true background sync.
253
+ - **No live cross-tab queue state.** The cross-tab _lock_ (preventing double-sends) is real and
254
+ tested against real multiple tabs (see `e2e/`) — but `queue.list()` in tab B doesn't reactively
255
+ update when tab A's queue changes; you'd need to poll or re-call it.
256
+ - **Storage quota** is enforced via `maxQueueItemSizeBytes` (rejects oversized items explicitly,
257
+ default 5 MB) rather than proactively checked against `navigator.storage.estimate()` — a
258
+ same-origin quota that's already nearly full can still surface as an `onError` `'db-operation'`
259
+ event rather than being caught in advance.
260
+ - **No per-endpoint circuit breaker.** Many queued items to the same persistently-down host each
261
+ retry independently (with jitter) rather than coordinating — fine at normal scale, untested at
262
+ very large queue sizes against one failing endpoint.
263
+
264
+ See [`ROADMAP.md`](./ROADMAP.md) for what's deliberately deferred and why.
233
265
 
234
266
  ## Bundle size
235
267
 
@@ -260,7 +292,8 @@ quality search) is never pulled in by the root import; you opt in explicitly via
260
292
 
261
293
  ## Contributing
262
294
 
263
- See [CONTRIBUTING.md](./CONTRIBUTING.md).
295
+ See [CONTRIBUTING.md](./CONTRIBUTING.md), [VERSIONING.md](./VERSIONING.md) for the (fully
296
+ automated) release process, and [SECURITY.md](./SECURITY.md) to report a vulnerability.
264
297
 
265
298
  ## License
266
299
 
@@ -1,4 +1,4 @@
1
- import { C as ConnectionInfo, a as ConnectionListener, U as Unsubscribe, R as RequestPriority, c as RetryBackoffConfig } from './types--FRrBa-i.cjs';
1
+ import { C as ConnectionInfo, a as ConnectionListener, U as Unsubscribe, R as RequestPriority, d as RetryBackoffConfig, L as LowdataErrorHandler } from './types-D6fjl8Wy.cjs';
2
2
 
3
3
  interface ConnectionMonitorOptions {
4
4
  /**
@@ -115,6 +115,12 @@ interface LowdataClientConfig {
115
115
  url: string;
116
116
  method: HttpMethod;
117
117
  }) => boolean;
118
+ /**
119
+ * Observe otherwise-silent internal failures (a background sync error, a fallback to an
120
+ * in-memory queue/draft store) instead of them only producing a single `console.warn`. Useful
121
+ * for piping into your own logging/monitoring in production.
122
+ */
123
+ onError?: LowdataErrorHandler;
118
124
  }
119
125
 
120
126
  interface QueueListFilter {
@@ -128,7 +134,7 @@ interface QueueListFilter {
128
134
  declare class RequestQueue {
129
135
  private memory;
130
136
  private accessor;
131
- constructor(getDb: () => Promise<IDBDatabase>);
137
+ constructor(getDb: () => Promise<IDBDatabase>, onError?: LowdataErrorHandler);
132
138
  isPersistent(): boolean;
133
139
  add(item: QueueItem): Promise<QueueItem>;
134
140
  update(item: QueueItem): Promise<void>;
@@ -1,4 +1,4 @@
1
- import { C as ConnectionInfo, a as ConnectionListener, U as Unsubscribe, R as RequestPriority, c as RetryBackoffConfig } from './types--FRrBa-i.js';
1
+ import { C as ConnectionInfo, a as ConnectionListener, U as Unsubscribe, R as RequestPriority, d as RetryBackoffConfig, L as LowdataErrorHandler } from './types-D6fjl8Wy.js';
2
2
 
3
3
  interface ConnectionMonitorOptions {
4
4
  /**
@@ -115,6 +115,12 @@ interface LowdataClientConfig {
115
115
  url: string;
116
116
  method: HttpMethod;
117
117
  }) => boolean;
118
+ /**
119
+ * Observe otherwise-silent internal failures (a background sync error, a fallback to an
120
+ * in-memory queue/draft store) instead of them only producing a single `console.warn`. Useful
121
+ * for piping into your own logging/monitoring in production.
122
+ */
123
+ onError?: LowdataErrorHandler;
118
124
  }
119
125
 
120
126
  interface QueueListFilter {
@@ -128,7 +134,7 @@ interface QueueListFilter {
128
134
  declare class RequestQueue {
129
135
  private memory;
130
136
  private accessor;
131
- constructor(getDb: () => Promise<IDBDatabase>);
137
+ constructor(getDb: () => Promise<IDBDatabase>, onError?: LowdataErrorHandler);
132
138
  isPersistent(): boolean;
133
139
  add(item: QueueItem): Promise<QueueItem>;
134
140
  update(item: QueueItem): Promise<void>;
package/dist/forms.cjs CHANGED
@@ -256,16 +256,23 @@ function getSharedDb() {
256
256
  }
257
257
  return sharedDbPromise;
258
258
  }
259
- function createDbFallbackAccessor(getDb) {
259
+ function createDbFallbackAccessor(getDb, onError) {
260
260
  let dbAvailable = true;
261
261
  return {
262
262
  async run(fn, fallback) {
263
263
  if (!dbAvailable) return fallback();
264
+ let db;
264
265
  try {
265
- const db = await getDb();
266
- return await fn(db);
267
- } catch {
266
+ db = await getDb();
267
+ } catch (error) {
268
268
  dbAvailable = false;
269
+ onError == null ? void 0 : onError(error, { scope: "db-open" });
270
+ return fallback();
271
+ }
272
+ try {
273
+ return await fn(db);
274
+ } catch (error) {
275
+ onError == null ? void 0 : onError(error, { scope: "db-operation" });
269
276
  return fallback();
270
277
  }
271
278
  },
@@ -291,9 +298,9 @@ var LowdataRequestError = class extends Error {
291
298
  var STORE = "queue";
292
299
  var PRIORITY_ORDER = { high: 0, normal: 1, low: 2 };
293
300
  var RequestQueue = class {
294
- constructor(getDb) {
301
+ constructor(getDb, onError) {
295
302
  this.memory = /* @__PURE__ */ new Map();
296
- this.accessor = createDbFallbackAccessor(getDb);
303
+ this.accessor = createDbFallbackAccessor(getDb, onError);
297
304
  }
298
305
  isPersistent() {
299
306
  return this.accessor.isPersistent();
@@ -608,12 +615,16 @@ var SyncManager = class {
608
615
  * or safety poll simply tries again.
609
616
  */
610
617
  async drain() {
611
- var _a, _b, _c, _d;
618
+ var _a, _b, _c, _d, _e, _f;
612
619
  if (this.draining || this.disposed) return;
613
620
  if (this.opts.connection.getStatus().quality === "offline") return;
614
621
  this.draining = true;
615
622
  try {
616
- const db = await this.opts.getDb().catch(() => void 0);
623
+ const db = await this.opts.getDb().catch((error) => {
624
+ var _a2, _b2;
625
+ (_b2 = (_a2 = this.opts).onError) == null ? void 0 : _b2.call(_a2, error, { scope: "db-open" });
626
+ return void 0;
627
+ });
617
628
  const lock = await acquireSyncLock(db, SYNC_LOCK_NAME, this.ownerId);
618
629
  if (!lock) return;
619
630
  let succeeded = 0;
@@ -636,7 +647,8 @@ var SyncManager = class {
636
647
  await lock.release().catch(() => {
637
648
  });
638
649
  }
639
- } catch {
650
+ } catch (error) {
651
+ (_f = (_e = this.opts).onError) == null ? void 0 : _f.call(_e, error, { scope: "sync" });
640
652
  } finally {
641
653
  this.draining = false;
642
654
  }
@@ -722,6 +734,9 @@ var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"
722
734
  function defaultShouldQueueOffline({ method }) {
723
735
  return MUTATING_METHODS.has(method);
724
736
  }
737
+ var defaultOnError = (error, { scope }) => {
738
+ console.warn(`lowdata: internal error (${scope})`, error);
739
+ };
725
740
  function normalizeHeaders(headers) {
726
741
  if (!headers) return void 0;
727
742
  if (headers instanceof Headers) {
@@ -750,15 +765,18 @@ var LowdataClient = class {
750
765
  this.config = config;
751
766
  this.syncEmitter = new Emitter();
752
767
  this.destroyed = false;
768
+ var _a;
769
+ const onError = (_a = config.onError) != null ? _a : defaultOnError;
753
770
  this.monitor = new ConnectionMonitor(config.connection);
754
- this.requestQueue = new RequestQueue(() => getSharedDb());
771
+ this.requestQueue = new RequestQueue(() => getSharedDb(), onError);
755
772
  this.syncManager = new SyncManager({
756
773
  queue: this.requestQueue,
757
774
  connection: this.monitor,
758
775
  getDb: () => getSharedDb(),
759
776
  retryConfig: config.retry,
760
777
  syncConcurrency: config.syncConcurrency,
761
- onEvent: (event) => this.syncEmitter.emit(event)
778
+ onEvent: (event) => this.syncEmitter.emit(event),
779
+ onError
762
780
  });
763
781
  this.connection = {
764
782
  getStatus: () => this.monitor.getStatus(),