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.
- package/README.md +37 -4
- package/dist/{client-CqO3Y1J5.d.cts → client-BmsR0Pf6.d.cts} +8 -2
- package/dist/{client-BvdWSYIM.d.ts → client-DSKoYUj9.d.ts} +8 -2
- package/dist/forms.cjs +29 -11
- package/dist/forms.cjs.map +1 -1
- package/dist/forms.d.cts +4 -4
- package/dist/forms.d.ts +4 -4
- package/dist/forms.js +29 -11
- package/dist/forms.js.map +1 -1
- package/dist/index.cjs +29 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +29 -11
- package/dist/index.js.map +1 -1
- package/dist/media.cjs +1 -1
- package/dist/media.cjs.map +1 -1
- package/dist/media.d.cts +2 -2
- package/dist/media.d.ts +2 -2
- package/dist/media.js +1 -1
- package/dist/media.js.map +1 -1
- package/dist/network.cjs +29 -11
- package/dist/network.cjs.map +1 -1
- package/dist/network.d.cts +7 -6
- package/dist/network.d.ts +7 -6
- package/dist/network.js +29 -11
- package/dist/network.js.map +1 -1
- package/dist/{progressiveImage-BhY_K8Dj.d.ts → progressiveImage-BOdic4dJ.d.ts} +1 -1
- package/dist/{progressiveImage-BWNdewbi.d.cts → progressiveImage-CDpz_rv3.d.cts} +1 -1
- package/dist/react.cjs +29 -11
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +4 -4
- package/dist/react.d.ts +4 -4
- package/dist/react.js +29 -11
- package/dist/react.js.map +1 -1
- package/dist/{retry-C3zL9T5Z.d.cts → retry-BnuESos0.d.cts} +1 -1
- package/dist/{retry-D6DfKGOi.d.ts → retry-CnvZhHan.d.ts} +1 -1
- package/dist/{types-Bn1BAcch.d.ts → types-1ha2HIfv.d.ts} +1 -1
- package/dist/{types-CZDYS-fB.d.cts → types-Cbm1nNDO.d.cts} +1 -1
- package/dist/{types--FRrBa-i.d.cts → types-D6fjl8Wy.d.cts} +22 -1
- package/dist/{types--FRrBa-i.d.ts → types-D6fjl8Wy.d.ts} +22 -1
- 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
|
-
[](https://github.com/bassmoses/lowdata/actions/workflows/ci.yml)
|
|
10
|
+
[](https://www.npmjs.com/package/lowdata)
|
|
11
|
+
[](./LICENSE)
|
|
10
12
|
[](#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
|
-
|
|
232
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
266
|
-
|
|
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(() =>
|
|
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(),
|