lowdata 0.1.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/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/client-BvdWSYIM.d.ts +187 -0
- package/dist/client-CqO3Y1J5.d.cts +187 -0
- package/dist/forms.cjs +1090 -0
- package/dist/forms.cjs.map +1 -0
- package/dist/forms.d.cts +29 -0
- package/dist/forms.d.ts +29 -0
- package/dist/forms.js +1088 -0
- package/dist/forms.js.map +1 -0
- package/dist/index.cjs +1109 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1099 -0
- package/dist/index.js.map +1 -0
- package/dist/media.cjs +278 -0
- package/dist/media.cjs.map +1 -0
- package/dist/media.d.cts +35 -0
- package/dist/media.d.ts +35 -0
- package/dist/media.js +274 -0
- package/dist/media.js.map +1 -0
- package/dist/network.cjs +923 -0
- package/dist/network.cjs.map +1 -0
- package/dist/network.d.cts +46 -0
- package/dist/network.d.ts +46 -0
- package/dist/network.js +912 -0
- package/dist/network.js.map +1 -0
- package/dist/progressiveImage-BWNdewbi.d.cts +23 -0
- package/dist/progressiveImage-BhY_K8Dj.d.ts +23 -0
- package/dist/react.cjs +1195 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +33 -0
- package/dist/react.d.ts +33 -0
- package/dist/react.js +1190 -0
- package/dist/react.js.map +1 -0
- package/dist/retry-C3zL9T5Z.d.cts +22 -0
- package/dist/retry-D6DfKGOi.d.ts +22 -0
- package/dist/types--FRrBa-i.d.cts +39 -0
- package/dist/types--FRrBa-i.d.ts +39 -0
- package/dist/types-Bn1BAcch.d.ts +33 -0
- package/dist/types-CZDYS-fB.d.cts +33 -0
- package/package.json +97 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lowdata contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# lowdata
|
|
2
|
+
|
|
3
|
+
**Resilience for the other half of the internet.**
|
|
4
|
+
|
|
5
|
+
Automatic retries with backoff, an offline request queue that survives page reloads, offline-safe
|
|
6
|
+
forms, and bandwidth-aware image compression — for web apps that have to keep working on 2G, on a
|
|
7
|
+
flaky café Wi-Fi, or mid-load-shedding. Framework-agnostic, near-zero dependencies, fully typed.
|
|
8
|
+
|
|
9
|
+
[](./LICENSE)
|
|
10
|
+
[](#bundle-size)
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { createLowdataClient } from 'lowdata';
|
|
14
|
+
|
|
15
|
+
const client = createLowdataClient();
|
|
16
|
+
await client.fetch('/api/orders', { method: 'POST', body: JSON.stringify(order) });
|
|
17
|
+
// Online: sends immediately, retrying transient failures automatically.
|
|
18
|
+
// Offline (or the server keeps failing): queued to IndexedDB and sent the moment
|
|
19
|
+
// connectivity returns — even if the page was reloaded in the meantime.
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Why lowdata
|
|
23
|
+
|
|
24
|
+
Most fetch/UI code silently assumes a fast, stable connection. In much of the world — including
|
|
25
|
+
huge parts of Africa — that assumption breaks constantly:
|
|
26
|
+
|
|
27
|
+
- **Networks drop mid-session.** A form submit or upload just... fails, and the input is gone.
|
|
28
|
+
- **Mobile data is expensive.** Every retry, every full-size photo upload, costs real money.
|
|
29
|
+
- **"Slow" is the normal case, not the edge case.** 2G/3G and congested Wi-Fi are common, not rare.
|
|
30
|
+
- **Reloads happen.** Low-end devices and unstable networks mean tabs get killed and reopened —
|
|
31
|
+
in-progress form input and queued requests need to survive that.
|
|
32
|
+
|
|
33
|
+
lowdata doesn't try to be a full offline-first framework or a service-worker-based PWA toolkit. It
|
|
34
|
+
solves the four concrete problems above, as simply as possible, and gets out of your way otherwise.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm add lowdata
|
|
40
|
+
# or: npm install lowdata / yarn add lowdata
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
React is an **optional** peer dependency — only needed if you use the `lowdata/react` hooks.
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
### A shop owner in Lagos syncing inventory over 2G
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { createLowdataClient, isQueued } from 'lowdata';
|
|
51
|
+
|
|
52
|
+
const client = createLowdataClient({ baseUrl: 'https://api.example.com' });
|
|
53
|
+
|
|
54
|
+
const result = await client.fetch('/inventory/restock', {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
body: JSON.stringify({ sku: 'RICE-25KG', delta: 40 }),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (isQueued(result)) {
|
|
60
|
+
console.log('Saved locally — will sync automatically once the connection is back.');
|
|
61
|
+
} else {
|
|
62
|
+
console.log('Sent immediately:', result.status);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
No polling, no manual retry loop, no lost restock entries when the shop's connection drops mid-tap.
|
|
67
|
+
|
|
68
|
+
### A rural clinic intake form that never loses data
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { createOfflineForm } from 'lowdata';
|
|
72
|
+
|
|
73
|
+
const form = createOfflineForm({ id: 'patient-intake', endpoint: '/api/patients' });
|
|
74
|
+
|
|
75
|
+
// Call on every field change — cheap, local, and survives a reload or a crashed tab.
|
|
76
|
+
await form.save({ name, age, symptoms });
|
|
77
|
+
|
|
78
|
+
// When the nurse taps "submit": sends now if possible, otherwise queues and syncs later.
|
|
79
|
+
const { status } = await form.submit({ name, age, symptoms });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
// React:
|
|
84
|
+
import { useOfflineForm } from 'lowdata/react';
|
|
85
|
+
|
|
86
|
+
function IntakeForm() {
|
|
87
|
+
const { status, submit } = useOfflineForm({ id: 'patient-intake', endpoint: '/api/patients' });
|
|
88
|
+
// status: 'idle' | 'saved' | 'pending' | 'syncing' | 'failed' | 'success'
|
|
89
|
+
return <StatusBadge status={status} />;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### A marketplace seller uploading a product photo without burning their data bundle
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { compressImage } from 'lowdata/media';
|
|
97
|
+
|
|
98
|
+
const { blob, sizeBytes } = await compressImage(photoFile, {
|
|
99
|
+
connectionAware: true, // aggressive on 'slow'/'offline', lighter-touch on 'online'
|
|
100
|
+
targetSizeKB: 200,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await client.fetch('/listings/photo', { method: 'POST', body: blob });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Core concepts
|
|
107
|
+
|
|
108
|
+
### Connection detection
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { getConnectionQuality, onConnectionChange } from 'lowdata';
|
|
112
|
+
|
|
113
|
+
getConnectionQuality(); // { quality: 'online' | 'slow' | 'offline', online, effectiveType?, ... }
|
|
114
|
+
const unsubscribe = onConnectionChange((info) => console.log(info.quality));
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Quality is computed from `navigator.onLine`/online-offline events (universal) plus
|
|
118
|
+
`navigator.connection` where available (Chromium browsers). On Safari/Firefox, `'slow'` detection
|
|
119
|
+
needs an **opt-in** latency probe — never automatic, since every probe costs a little data:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
createLowdataClient({ connection: { pingUrl: '/healthz', slowRttThresholdMs: 600 } });
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Offline queue & sync
|
|
126
|
+
|
|
127
|
+
Every mutating request (`POST`/`PUT`/`PATCH`/`DELETE`) that can't be delivered — offline, or the
|
|
128
|
+
server keeps failing — is written to IndexedDB and retried automatically, with priority ordering,
|
|
129
|
+
exponential backoff + jitter, and a cross-tab lock so two open tabs never double-send the same item:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
await client.queue.add({ url: '/api/x', method: 'POST', priority: 'high', body: json });
|
|
133
|
+
await client.queue.list({ status: 'pending' });
|
|
134
|
+
await client.queue.cancel(id);
|
|
135
|
+
|
|
136
|
+
client.onSync((event) => {
|
|
137
|
+
// 'sync-start' | 'item-start' | 'item-success' | 'item-failed' | 'sync-complete'
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Retry & backoff
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
createLowdataClient({
|
|
145
|
+
retry: { maxRetries: 8, baseDelayMs: 500, maxDelayMs: 30_000, jitter: 'full' },
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Retries network errors, timeouts, and `429`/`502`/`503`/`504` (honoring `Retry-After`); `4xx`
|
|
150
|
+
responses are returned to you immediately, unretried, so you can handle validation errors normally.
|
|
151
|
+
|
|
152
|
+
### Offline forms
|
|
153
|
+
|
|
154
|
+
`createOfflineForm` composes `save()` (local, instant, reload-safe) with `submit()` (send now or
|
|
155
|
+
queue) and projects the queue's sync events into a simple status: `idle → saved → pending → syncing
|
|
156
|
+
→ success`, with `failed`/`retry()` on the unhappy path.
|
|
157
|
+
|
|
158
|
+
### Media compression & progressive images
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { compressImage, createProgressiveImageLoader } from 'lowdata/media';
|
|
162
|
+
|
|
163
|
+
const loader = createProgressiveImageLoader({ src: fullImageUrl, placeholder: tinyBlurDataUrl });
|
|
164
|
+
loader.subscribe(({ src, isLoaded }) => setImgSrc(src));
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## API reference
|
|
168
|
+
|
|
169
|
+
| Subpath | Exports |
|
|
170
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
171
|
+
| `lowdata` | `createLowdataClient`, `LowdataClient`, `isQueued`, `LowdataRequestError`, `createOfflineForm`, `getConnectionQuality`, `onConnectionChange`, core types |
|
|
172
|
+
| `lowdata/network` | Everything in the root, plus `RequestQueue`, `SyncManager`, `ConnectionMonitor`, `defaultRetryOn` |
|
|
173
|
+
| `lowdata/forms` | `createOfflineForm`, form types |
|
|
174
|
+
| `lowdata/media` | `compressImage`, `createProgressiveImageLoader`, `presetForQuality` |
|
|
175
|
+
| `lowdata/react` | `useConnectionStatus`, `useLowdataClient`, `useOfflineForm`, `useProgressiveImage` |
|
|
176
|
+
|
|
177
|
+
Full type signatures are in each subpath's shipped `.d.ts` — every export is documented with TSDoc.
|
|
178
|
+
|
|
179
|
+
## Recipes
|
|
180
|
+
|
|
181
|
+
**Prioritize a request:**
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
await client.fetch('/urgent', { method: 'POST', body, priority: 'high' });
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Cancel a stale request:**
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const result = await client.queue.add({ url, method: 'POST', priority: 'normal', body });
|
|
191
|
+
await client.queue.cancel(result.id);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Custom retry policy per request:**
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
await client.fetch(url, { method: 'POST', body, retry: { maxRetries: 2 } });
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Server-side idempotency:** every queued/retried request carries an `idempotencyKey` (defaulting
|
|
201
|
+
to the item's id for form submissions) — have your backend dedupe on it, since a retried or
|
|
202
|
+
cross-tab-raced request is still possible in rare edge cases.
|
|
203
|
+
|
|
204
|
+
## Framework guides
|
|
205
|
+
|
|
206
|
+
- **Vanilla JS / any framework:** use `lowdata`/`lowdata/forms`/`lowdata/media` directly — no
|
|
207
|
+
framework glue needed.
|
|
208
|
+
- **React:** `lowdata/react` ships thin hooks over the framework-agnostic core.
|
|
209
|
+
- **Vue:** no dedicated subpath yet — wrap the core in a composable:
|
|
210
|
+
```ts
|
|
211
|
+
import { ref, onUnmounted } from 'vue';
|
|
212
|
+
import { onConnectionChange, getConnectionQuality } from 'lowdata';
|
|
213
|
+
|
|
214
|
+
export function useConnectionStatus() {
|
|
215
|
+
const status = ref(getConnectionQuality());
|
|
216
|
+
const unsubscribe = onConnectionChange((info) => (status.value = info));
|
|
217
|
+
onUnmounted(unsubscribe);
|
|
218
|
+
return status;
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Browser & runtime support
|
|
223
|
+
|
|
224
|
+
- Requires `fetch`, `AbortController`, and `Promise` — all standard in any target browser.
|
|
225
|
+
- **IndexedDB** persists the offline queue and form drafts. Where it's unavailable (some SSR
|
|
226
|
+
contexts, locked-down private-browsing modes), lowdata falls back to an in-memory queue with a
|
|
227
|
+
console warning instead of throwing — nothing breaks, offline persistence is just unavailable
|
|
228
|
+
for that session.
|
|
229
|
+
- **SSR-safe to import**: `createLowdataClient()` and friends never assume `window`/`navigator`
|
|
230
|
+
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.
|
|
233
|
+
|
|
234
|
+
## Bundle size
|
|
235
|
+
|
|
236
|
+
Sizes below are the unminified ESM build's gzip size — real-world minified size (via your app's
|
|
237
|
+
bundler) will be smaller. Each subpath is independently tree-shakeable; you only pay for what you
|
|
238
|
+
import.
|
|
239
|
+
|
|
240
|
+
| Subpath | gzip (unminified) |
|
|
241
|
+
| ----------------------------------------------- | ----------------- |
|
|
242
|
+
| `lowdata` (core + network + forms) | ~9.3 KB |
|
|
243
|
+
| `lowdata/network` alone | ~8.1 KB |
|
|
244
|
+
| `lowdata/media` alone | ~2.7 KB |
|
|
245
|
+
| `lowdata/react` (adds hooks over network+forms) | ~9.8 KB |
|
|
246
|
+
|
|
247
|
+
`lowdata/media`'s image compression (the heaviest single feature — canvas resize + iterative
|
|
248
|
+
quality search) is never pulled in by the root import; you opt in explicitly via `lowdata/media`.
|
|
249
|
+
|
|
250
|
+
## lowdata vs. alternatives
|
|
251
|
+
|
|
252
|
+
- **vs. Service Worker background sync:** lowdata needs no service worker registration, no
|
|
253
|
+
separate sync event handler, no HTTPS-only constraint for local dev — at the cost of only syncing
|
|
254
|
+
while a tab is open. If you need true background sync after the tab closes, pair lowdata's queue
|
|
255
|
+
format with your own service worker, or wait for a future release.
|
|
256
|
+
- **vs. `axios-retry`/generic retry libraries:** those retry a single in-flight request; lowdata
|
|
257
|
+
additionally persists failed/offline requests to survive a reload and syncs them automatically.
|
|
258
|
+
- **vs. building it yourself:** this is the boring, well-tested version of the offline queue +
|
|
259
|
+
retry + form-status code most apps end up hand-rolling anyway.
|
|
260
|
+
|
|
261
|
+
## Contributing
|
|
262
|
+
|
|
263
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
264
|
+
|
|
265
|
+
## License
|
|
266
|
+
|
|
267
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { C as ConnectionInfo, a as ConnectionListener, U as Unsubscribe, R as RequestPriority, c as RetryBackoffConfig } from './types--FRrBa-i.js';
|
|
2
|
+
|
|
3
|
+
interface ConnectionMonitorOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Opt-in latency probe URL used to detect 'slow' connections on browsers without the Network
|
|
6
|
+
* Information API (Safari, Firefox). Never probed automatically on a timer — only once on
|
|
7
|
+
* startup and once per reconnect — because every probe costs a little data.
|
|
8
|
+
*/
|
|
9
|
+
pingUrl?: string;
|
|
10
|
+
/** Round-trip time above which the ping probe classifies the connection as 'slow'. Default 600ms. */
|
|
11
|
+
slowRttThresholdMs?: number;
|
|
12
|
+
/** downlink (Mbps) below which the Network Information API classifies as 'slow'. Default 0.5. */
|
|
13
|
+
slowDownlinkMbps?: number;
|
|
14
|
+
/** Timeout for the ping probe itself, in ms. Default 5000. */
|
|
15
|
+
pingTimeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Tracks connection quality (online / slow / offline) using the best signal available:
|
|
19
|
+
* `navigator.onLine` + online/offline events as the universal baseline, `navigator.connection`
|
|
20
|
+
* where present, and an optional opt-in ping probe as a cross-browser fallback for 'slow'.
|
|
21
|
+
*/
|
|
22
|
+
declare class ConnectionMonitor {
|
|
23
|
+
private emitter;
|
|
24
|
+
private current;
|
|
25
|
+
private probedRttMs;
|
|
26
|
+
private readonly options;
|
|
27
|
+
private disposed;
|
|
28
|
+
private onlineHandler?;
|
|
29
|
+
private offlineHandler?;
|
|
30
|
+
private connectionChangeHandler?;
|
|
31
|
+
constructor(options?: ConnectionMonitorOptions);
|
|
32
|
+
getStatus(): ConnectionInfo;
|
|
33
|
+
subscribe(listener: ConnectionListener): Unsubscribe;
|
|
34
|
+
/** Manually re-run the opt-in ping probe (no-op if no `pingUrl` was configured). */
|
|
35
|
+
probeNow(): Promise<ConnectionInfo>;
|
|
36
|
+
destroy(): void;
|
|
37
|
+
private refresh;
|
|
38
|
+
private computeInfo;
|
|
39
|
+
}
|
|
40
|
+
/** Convenience one-shot read of connection quality, without owning a `LowdataClient`. */
|
|
41
|
+
declare function getConnectionQuality(): ConnectionInfo;
|
|
42
|
+
/** Convenience subscription to connection changes, without owning a `LowdataClient`. */
|
|
43
|
+
declare function onConnectionChange(listener: ConnectionListener): Unsubscribe;
|
|
44
|
+
|
|
45
|
+
type QueueItemStatus = 'pending' | 'sending' | 'failed' | 'done' | 'cancelled';
|
|
46
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
47
|
+
interface QueueItem {
|
|
48
|
+
id: string;
|
|
49
|
+
url: string;
|
|
50
|
+
method: HttpMethod;
|
|
51
|
+
headers?: Record<string, string>;
|
|
52
|
+
body?: string | Blob | null;
|
|
53
|
+
priority: RequestPriority;
|
|
54
|
+
createdAt: number;
|
|
55
|
+
updatedAt: number;
|
|
56
|
+
attempts: number;
|
|
57
|
+
status: QueueItemStatus;
|
|
58
|
+
nextAttemptAt: number;
|
|
59
|
+
lastError?: string;
|
|
60
|
+
meta?: Record<string, unknown>;
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
retry?: Partial<RetryBackoffConfig>;
|
|
63
|
+
idempotencyKey?: string;
|
|
64
|
+
}
|
|
65
|
+
interface EnqueueOptions {
|
|
66
|
+
priority?: RequestPriority;
|
|
67
|
+
meta?: Record<string, unknown>;
|
|
68
|
+
timeoutMs?: number;
|
|
69
|
+
retry?: Partial<RetryBackoffConfig>;
|
|
70
|
+
idempotencyKey?: string;
|
|
71
|
+
/** Skip the live attempt and go straight to the persistent queue, even if currently online. */
|
|
72
|
+
forceQueue?: boolean;
|
|
73
|
+
}
|
|
74
|
+
/** Result returned by `client.fetch()` when a request could not be sent live and was queued instead. */
|
|
75
|
+
interface QueuedResult {
|
|
76
|
+
queued: true;
|
|
77
|
+
id: string;
|
|
78
|
+
item: QueueItem;
|
|
79
|
+
}
|
|
80
|
+
declare function isQueued(result: Response | QueuedResult): result is QueuedResult;
|
|
81
|
+
type SyncEvent = {
|
|
82
|
+
type: 'sync-start';
|
|
83
|
+
pending: number;
|
|
84
|
+
} | {
|
|
85
|
+
type: 'item-start';
|
|
86
|
+
item: QueueItem;
|
|
87
|
+
} | {
|
|
88
|
+
type: 'item-success';
|
|
89
|
+
item: QueueItem;
|
|
90
|
+
} | {
|
|
91
|
+
type: 'item-failed';
|
|
92
|
+
item: QueueItem;
|
|
93
|
+
willRetry: boolean;
|
|
94
|
+
} | {
|
|
95
|
+
type: 'sync-complete';
|
|
96
|
+
succeeded: number;
|
|
97
|
+
failed: number;
|
|
98
|
+
};
|
|
99
|
+
interface LowdataClientConfig {
|
|
100
|
+
/** Prefixed onto every relative URL passed to `fetch()`/`queue.add()`. */
|
|
101
|
+
baseUrl?: string;
|
|
102
|
+
defaultHeaders?: Record<string, string>;
|
|
103
|
+
retry?: Partial<RetryBackoffConfig>;
|
|
104
|
+
connection?: ConnectionMonitorOptions;
|
|
105
|
+
/** How many queued items to send concurrently during a sync drain. Default 1 (conservative). */
|
|
106
|
+
syncConcurrency?: number;
|
|
107
|
+
/** Reject `queue.add()` for payloads larger than this. Default 5 MB. */
|
|
108
|
+
maxQueueItemSizeBytes?: number;
|
|
109
|
+
/**
|
|
110
|
+
* Decide whether a failed/offline request should be queued for later instead of surfaced as an
|
|
111
|
+
* error. Default: only mutating methods (POST/PUT/PATCH/DELETE) are queued; GET/HEAD are not,
|
|
112
|
+
* since queuing a read rarely makes sense.
|
|
113
|
+
*/
|
|
114
|
+
shouldQueueOffline?: (input: {
|
|
115
|
+
url: string;
|
|
116
|
+
method: HttpMethod;
|
|
117
|
+
}) => boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface QueueListFilter {
|
|
121
|
+
status?: QueueItemStatus;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Persistent (IndexedDB-backed) request queue with an automatic in-memory fallback for
|
|
125
|
+
* environments without IndexedDB (SSR, very old/locked-down browsers). The fallback is
|
|
126
|
+
* non-persistent — it exists so importing lowdata never throws, not to promise durability there.
|
|
127
|
+
*/
|
|
128
|
+
declare class RequestQueue {
|
|
129
|
+
private memory;
|
|
130
|
+
private accessor;
|
|
131
|
+
constructor(getDb: () => Promise<IDBDatabase>);
|
|
132
|
+
isPersistent(): boolean;
|
|
133
|
+
add(item: QueueItem): Promise<QueueItem>;
|
|
134
|
+
update(item: QueueItem): Promise<void>;
|
|
135
|
+
get(id: string): Promise<QueueItem | undefined>;
|
|
136
|
+
remove(id: string): Promise<void>;
|
|
137
|
+
/** Filtering by status queries the `status` index rather than scanning the whole store. */
|
|
138
|
+
list(filter?: QueueListFilter): Promise<QueueItem[]>;
|
|
139
|
+
clear(): Promise<void>;
|
|
140
|
+
/** Items ready to send now: `pending` and due, sorted by priority then insertion order. */
|
|
141
|
+
selectEligible(now: number): Promise<QueueItem[]>;
|
|
142
|
+
/** Revive items stuck in `sending` longer than `staleAfterMs` (recovers from a crash mid-send). */
|
|
143
|
+
sweepStale(staleAfterMs: number, now: number): Promise<QueueItem[]>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
declare class LowdataClient {
|
|
147
|
+
private config;
|
|
148
|
+
readonly connection: {
|
|
149
|
+
getStatus: () => ConnectionInfo;
|
|
150
|
+
subscribe: (listener: ConnectionListener) => Unsubscribe;
|
|
151
|
+
};
|
|
152
|
+
readonly queue: {
|
|
153
|
+
add: (item: Omit<QueueItem, 'id' | 'createdAt' | 'updatedAt' | 'attempts' | 'status' | 'nextAttemptAt'>) => Promise<QueueItem>;
|
|
154
|
+
cancel: (id: string) => Promise<void>;
|
|
155
|
+
list: (filter?: QueueListFilter) => Promise<QueueItem[]>;
|
|
156
|
+
clear: () => Promise<void>;
|
|
157
|
+
};
|
|
158
|
+
private monitor;
|
|
159
|
+
private requestQueue;
|
|
160
|
+
private syncManager;
|
|
161
|
+
private syncEmitter;
|
|
162
|
+
private destroyed;
|
|
163
|
+
constructor(config?: LowdataClientConfig);
|
|
164
|
+
onSync(listener: (event: SyncEvent) => void): Unsubscribe;
|
|
165
|
+
/**
|
|
166
|
+
* Drop-in `fetch()` wrapper: retries transient failures with backoff, and — for mutating
|
|
167
|
+
* requests that still can't get through — falls back to the persistent offline queue instead
|
|
168
|
+
* of losing the request. Returns a `QueuedResult` (check with `isQueued()`) when queued.
|
|
169
|
+
*/
|
|
170
|
+
fetch(url: string, init?: RequestInit & EnqueueOptions): Promise<Response | QueuedResult>;
|
|
171
|
+
destroy(): void;
|
|
172
|
+
private resolveUrl;
|
|
173
|
+
private maxQueueItemBytes;
|
|
174
|
+
private assertWithinSizeBudget;
|
|
175
|
+
/**
|
|
176
|
+
* Shared by `enqueueFromInit` (the `fetch()` fallback path) and `enqueue` (the direct
|
|
177
|
+
* `queue.add()` path) — the only difference between the two call sites is where the fields come
|
|
178
|
+
* from, not how a queue item gets constructed, persisted, and announced to the sync manager.
|
|
179
|
+
*/
|
|
180
|
+
private persistNewQueueItem;
|
|
181
|
+
private enqueueFromInit;
|
|
182
|
+
private enqueue;
|
|
183
|
+
private cancelQueued;
|
|
184
|
+
}
|
|
185
|
+
declare function createLowdataClient(config?: LowdataClientConfig): LowdataClient;
|
|
186
|
+
|
|
187
|
+
export { ConnectionMonitor as C, type EnqueueOptions as E, type HttpMethod as H, LowdataClient as L, type QueueItem as Q, RequestQueue as R, type SyncEvent as S, type ConnectionMonitorOptions as a, type LowdataClientConfig as b, type QueueItemStatus as c, type QueuedResult as d, createLowdataClient as e, type QueueListFilter as f, getConnectionQuality as g, isQueued as i, onConnectionChange as o };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { C as ConnectionInfo, a as ConnectionListener, U as Unsubscribe, R as RequestPriority, c as RetryBackoffConfig } from './types--FRrBa-i.cjs';
|
|
2
|
+
|
|
3
|
+
interface ConnectionMonitorOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Opt-in latency probe URL used to detect 'slow' connections on browsers without the Network
|
|
6
|
+
* Information API (Safari, Firefox). Never probed automatically on a timer — only once on
|
|
7
|
+
* startup and once per reconnect — because every probe costs a little data.
|
|
8
|
+
*/
|
|
9
|
+
pingUrl?: string;
|
|
10
|
+
/** Round-trip time above which the ping probe classifies the connection as 'slow'. Default 600ms. */
|
|
11
|
+
slowRttThresholdMs?: number;
|
|
12
|
+
/** downlink (Mbps) below which the Network Information API classifies as 'slow'. Default 0.5. */
|
|
13
|
+
slowDownlinkMbps?: number;
|
|
14
|
+
/** Timeout for the ping probe itself, in ms. Default 5000. */
|
|
15
|
+
pingTimeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Tracks connection quality (online / slow / offline) using the best signal available:
|
|
19
|
+
* `navigator.onLine` + online/offline events as the universal baseline, `navigator.connection`
|
|
20
|
+
* where present, and an optional opt-in ping probe as a cross-browser fallback for 'slow'.
|
|
21
|
+
*/
|
|
22
|
+
declare class ConnectionMonitor {
|
|
23
|
+
private emitter;
|
|
24
|
+
private current;
|
|
25
|
+
private probedRttMs;
|
|
26
|
+
private readonly options;
|
|
27
|
+
private disposed;
|
|
28
|
+
private onlineHandler?;
|
|
29
|
+
private offlineHandler?;
|
|
30
|
+
private connectionChangeHandler?;
|
|
31
|
+
constructor(options?: ConnectionMonitorOptions);
|
|
32
|
+
getStatus(): ConnectionInfo;
|
|
33
|
+
subscribe(listener: ConnectionListener): Unsubscribe;
|
|
34
|
+
/** Manually re-run the opt-in ping probe (no-op if no `pingUrl` was configured). */
|
|
35
|
+
probeNow(): Promise<ConnectionInfo>;
|
|
36
|
+
destroy(): void;
|
|
37
|
+
private refresh;
|
|
38
|
+
private computeInfo;
|
|
39
|
+
}
|
|
40
|
+
/** Convenience one-shot read of connection quality, without owning a `LowdataClient`. */
|
|
41
|
+
declare function getConnectionQuality(): ConnectionInfo;
|
|
42
|
+
/** Convenience subscription to connection changes, without owning a `LowdataClient`. */
|
|
43
|
+
declare function onConnectionChange(listener: ConnectionListener): Unsubscribe;
|
|
44
|
+
|
|
45
|
+
type QueueItemStatus = 'pending' | 'sending' | 'failed' | 'done' | 'cancelled';
|
|
46
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
47
|
+
interface QueueItem {
|
|
48
|
+
id: string;
|
|
49
|
+
url: string;
|
|
50
|
+
method: HttpMethod;
|
|
51
|
+
headers?: Record<string, string>;
|
|
52
|
+
body?: string | Blob | null;
|
|
53
|
+
priority: RequestPriority;
|
|
54
|
+
createdAt: number;
|
|
55
|
+
updatedAt: number;
|
|
56
|
+
attempts: number;
|
|
57
|
+
status: QueueItemStatus;
|
|
58
|
+
nextAttemptAt: number;
|
|
59
|
+
lastError?: string;
|
|
60
|
+
meta?: Record<string, unknown>;
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
retry?: Partial<RetryBackoffConfig>;
|
|
63
|
+
idempotencyKey?: string;
|
|
64
|
+
}
|
|
65
|
+
interface EnqueueOptions {
|
|
66
|
+
priority?: RequestPriority;
|
|
67
|
+
meta?: Record<string, unknown>;
|
|
68
|
+
timeoutMs?: number;
|
|
69
|
+
retry?: Partial<RetryBackoffConfig>;
|
|
70
|
+
idempotencyKey?: string;
|
|
71
|
+
/** Skip the live attempt and go straight to the persistent queue, even if currently online. */
|
|
72
|
+
forceQueue?: boolean;
|
|
73
|
+
}
|
|
74
|
+
/** Result returned by `client.fetch()` when a request could not be sent live and was queued instead. */
|
|
75
|
+
interface QueuedResult {
|
|
76
|
+
queued: true;
|
|
77
|
+
id: string;
|
|
78
|
+
item: QueueItem;
|
|
79
|
+
}
|
|
80
|
+
declare function isQueued(result: Response | QueuedResult): result is QueuedResult;
|
|
81
|
+
type SyncEvent = {
|
|
82
|
+
type: 'sync-start';
|
|
83
|
+
pending: number;
|
|
84
|
+
} | {
|
|
85
|
+
type: 'item-start';
|
|
86
|
+
item: QueueItem;
|
|
87
|
+
} | {
|
|
88
|
+
type: 'item-success';
|
|
89
|
+
item: QueueItem;
|
|
90
|
+
} | {
|
|
91
|
+
type: 'item-failed';
|
|
92
|
+
item: QueueItem;
|
|
93
|
+
willRetry: boolean;
|
|
94
|
+
} | {
|
|
95
|
+
type: 'sync-complete';
|
|
96
|
+
succeeded: number;
|
|
97
|
+
failed: number;
|
|
98
|
+
};
|
|
99
|
+
interface LowdataClientConfig {
|
|
100
|
+
/** Prefixed onto every relative URL passed to `fetch()`/`queue.add()`. */
|
|
101
|
+
baseUrl?: string;
|
|
102
|
+
defaultHeaders?: Record<string, string>;
|
|
103
|
+
retry?: Partial<RetryBackoffConfig>;
|
|
104
|
+
connection?: ConnectionMonitorOptions;
|
|
105
|
+
/** How many queued items to send concurrently during a sync drain. Default 1 (conservative). */
|
|
106
|
+
syncConcurrency?: number;
|
|
107
|
+
/** Reject `queue.add()` for payloads larger than this. Default 5 MB. */
|
|
108
|
+
maxQueueItemSizeBytes?: number;
|
|
109
|
+
/**
|
|
110
|
+
* Decide whether a failed/offline request should be queued for later instead of surfaced as an
|
|
111
|
+
* error. Default: only mutating methods (POST/PUT/PATCH/DELETE) are queued; GET/HEAD are not,
|
|
112
|
+
* since queuing a read rarely makes sense.
|
|
113
|
+
*/
|
|
114
|
+
shouldQueueOffline?: (input: {
|
|
115
|
+
url: string;
|
|
116
|
+
method: HttpMethod;
|
|
117
|
+
}) => boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface QueueListFilter {
|
|
121
|
+
status?: QueueItemStatus;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Persistent (IndexedDB-backed) request queue with an automatic in-memory fallback for
|
|
125
|
+
* environments without IndexedDB (SSR, very old/locked-down browsers). The fallback is
|
|
126
|
+
* non-persistent — it exists so importing lowdata never throws, not to promise durability there.
|
|
127
|
+
*/
|
|
128
|
+
declare class RequestQueue {
|
|
129
|
+
private memory;
|
|
130
|
+
private accessor;
|
|
131
|
+
constructor(getDb: () => Promise<IDBDatabase>);
|
|
132
|
+
isPersistent(): boolean;
|
|
133
|
+
add(item: QueueItem): Promise<QueueItem>;
|
|
134
|
+
update(item: QueueItem): Promise<void>;
|
|
135
|
+
get(id: string): Promise<QueueItem | undefined>;
|
|
136
|
+
remove(id: string): Promise<void>;
|
|
137
|
+
/** Filtering by status queries the `status` index rather than scanning the whole store. */
|
|
138
|
+
list(filter?: QueueListFilter): Promise<QueueItem[]>;
|
|
139
|
+
clear(): Promise<void>;
|
|
140
|
+
/** Items ready to send now: `pending` and due, sorted by priority then insertion order. */
|
|
141
|
+
selectEligible(now: number): Promise<QueueItem[]>;
|
|
142
|
+
/** Revive items stuck in `sending` longer than `staleAfterMs` (recovers from a crash mid-send). */
|
|
143
|
+
sweepStale(staleAfterMs: number, now: number): Promise<QueueItem[]>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
declare class LowdataClient {
|
|
147
|
+
private config;
|
|
148
|
+
readonly connection: {
|
|
149
|
+
getStatus: () => ConnectionInfo;
|
|
150
|
+
subscribe: (listener: ConnectionListener) => Unsubscribe;
|
|
151
|
+
};
|
|
152
|
+
readonly queue: {
|
|
153
|
+
add: (item: Omit<QueueItem, 'id' | 'createdAt' | 'updatedAt' | 'attempts' | 'status' | 'nextAttemptAt'>) => Promise<QueueItem>;
|
|
154
|
+
cancel: (id: string) => Promise<void>;
|
|
155
|
+
list: (filter?: QueueListFilter) => Promise<QueueItem[]>;
|
|
156
|
+
clear: () => Promise<void>;
|
|
157
|
+
};
|
|
158
|
+
private monitor;
|
|
159
|
+
private requestQueue;
|
|
160
|
+
private syncManager;
|
|
161
|
+
private syncEmitter;
|
|
162
|
+
private destroyed;
|
|
163
|
+
constructor(config?: LowdataClientConfig);
|
|
164
|
+
onSync(listener: (event: SyncEvent) => void): Unsubscribe;
|
|
165
|
+
/**
|
|
166
|
+
* Drop-in `fetch()` wrapper: retries transient failures with backoff, and — for mutating
|
|
167
|
+
* requests that still can't get through — falls back to the persistent offline queue instead
|
|
168
|
+
* of losing the request. Returns a `QueuedResult` (check with `isQueued()`) when queued.
|
|
169
|
+
*/
|
|
170
|
+
fetch(url: string, init?: RequestInit & EnqueueOptions): Promise<Response | QueuedResult>;
|
|
171
|
+
destroy(): void;
|
|
172
|
+
private resolveUrl;
|
|
173
|
+
private maxQueueItemBytes;
|
|
174
|
+
private assertWithinSizeBudget;
|
|
175
|
+
/**
|
|
176
|
+
* Shared by `enqueueFromInit` (the `fetch()` fallback path) and `enqueue` (the direct
|
|
177
|
+
* `queue.add()` path) — the only difference between the two call sites is where the fields come
|
|
178
|
+
* from, not how a queue item gets constructed, persisted, and announced to the sync manager.
|
|
179
|
+
*/
|
|
180
|
+
private persistNewQueueItem;
|
|
181
|
+
private enqueueFromInit;
|
|
182
|
+
private enqueue;
|
|
183
|
+
private cancelQueued;
|
|
184
|
+
}
|
|
185
|
+
declare function createLowdataClient(config?: LowdataClientConfig): LowdataClient;
|
|
186
|
+
|
|
187
|
+
export { ConnectionMonitor as C, type EnqueueOptions as E, type HttpMethod as H, LowdataClient as L, type QueueItem as Q, RequestQueue as R, type SyncEvent as S, type ConnectionMonitorOptions as a, type LowdataClientConfig as b, type QueueItemStatus as c, type QueuedResult as d, createLowdataClient as e, type QueueListFilter as f, getConnectionQuality as g, isQueued as i, onConnectionChange as o };
|