react-native-outbox-mutation-queue 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/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+ This project adheres to [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [Unreleased]
7
+
8
+ ### Added
9
+ - `deduped(kept, dropped, strategy)` event so collapsing a task is observable
10
+ rather than silent
11
+ - One-time development warning the first time deduplication discards a
12
+ payload, with `silenceDedupeWarning` to opt out
13
+ - Chat recipe in the guide, including why `dedupeKey` must not be used for
14
+ messages
15
+
16
+ ## [0.1.0] - 2026-09-03
17
+
18
+ Initial release.
19
+
20
+ ### Added
21
+ - Persistent offline mutation queue with a storage-agnostic adapter interface
22
+ - Exponential backoff with jitter and a configurable retry policy
23
+ - Deduplication strategies: `replace`, `drop`, `keep`
24
+ - `classifyError` to separate permanent failures from transient ones
25
+ - `onDiscard` conflict hook for exhausted or permanently failed tasks
26
+ - Connectivity control via `setOnline`, with no NetInfo dependency
27
+ - Interrupted in-flight tasks restored as pending on relaunch
28
+ - `useOfflineQueue` React hook for rendering sync state
29
+ - Runnable Node demo and an Expo example app
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Syed Arbab Ali Shah
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,345 @@
1
+ # react-native-outbox-mutation-queue
2
+
3
+ [![npm](https://img.shields.io/npm/v/react-native-outbox-mutation-queue.svg)](https://www.npmjs.com/package/react-native-outbox-mutation-queue)
4
+ [![CI](https://github.com/ARBAB1/react-native-outbox-mutation-queue/actions/workflows/ci.yml/badge.svg)](https://github.com/ARBAB1/react-native-outbox-mutation-queue/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/react-native-outbox-mutation-queue.svg)](./LICENSE)
6
+ [![types](https://img.shields.io/npm/types/react-native-outbox-mutation-queue.svg)](./src/types.ts)
7
+ [![zero deps](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](./package.json)
8
+
9
+ **The user tapped Save on a train. Now what?**
10
+
11
+ A focused offline mutation queue for React Native. It holds writes while the
12
+ device is offline, retries them with exponential backoff when it comes back,
13
+ collapses duplicates, and survives app restarts.
14
+
15
+ It is deliberately **not** a database sync engine.
16
+
17
+ ```ts
18
+ await queue.enqueue('updateProfile', { name: 'Ada' }, { dedupeKey: 'profile' });
19
+ // Offline? Queued. Online? Sent. Failed? Retried. App killed? Still there.
20
+ ```
21
+
22
+ ---
23
+
24
+ ## The problem
25
+
26
+ Imagine your app has a "Save" button.
27
+
28
+ Normally when someone taps Save, your app sends the data to the server right
29
+ then:
30
+
31
+ ```
32
+ User taps Save → app sends to server → server replies "ok" → done
33
+ ```
34
+
35
+ But what if there is no internet at that moment? In a lift, on the metro, in a
36
+ basement, bad signal.
37
+
38
+ The send fails. Your app shows "Error, try again." The user has already put
39
+ their phone in their pocket. **Their data is gone.**
40
+
41
+ ## What this library does
42
+
43
+ It sits in between:
44
+
45
+ ```
46
+ User taps Save → library stores it on the phone → shows "Saved" instantly
47
+
48
+ (waits until internet comes back)
49
+
50
+ sends to server automatically
51
+ ```
52
+
53
+ The user never sees an error. They do not have to retry. It just gets
54
+ delivered whenever the connection returns — even if they close the app
55
+ completely and open it tomorrow.
56
+
57
+ That is it. That is the whole idea.
58
+
59
+ ## What it handles for you
60
+
61
+ | Situation | What happens |
62
+ |---|---|
63
+ | **No internet** | Holds the data safely on the phone |
64
+ | **Internet comes back** | Sends it automatically |
65
+ | **Server is down** | Waits and tries again — 1s, then 2s, 4s, 8s. Not hammering it |
66
+ | **Server says "invalid"** | Stops trying, tells your app |
67
+ | **User saved 10 times quickly** | Sends only the last version, not all 10 |
68
+ | **User force-closed the app** | Still there when they reopen |
69
+
70
+ ---
71
+
72
+ ## Why this exists
73
+
74
+ Most offline problems in a mobile app are not "replicate my database." They
75
+ are one narrow thing: **a mutation fired at a moment when the network was not
76
+ there, and it must not be lost.**
77
+
78
+ Good options already exist — they just all come with a commitment:
79
+
80
+ | Package | Weekly downloads | What it asks of you |
81
+ |---|---|---|
82
+ | [`@tanstack/react-query`](https://tanstack.com/query) | ~65M | Adopt React Query for your data layer |
83
+ | [`@redux-offline/redux-offline`](https://github.com/redux-offline/redux-offline) | ~100k | Adopt Redux |
84
+ | [`rxdb`](https://rxdb.info) | ~73k | Adopt their database |
85
+ | [`@nozbe/watermelondb`](https://watermelondb.dev) | ~61k | Adopt their database |
86
+ | [`@powersync/react-native`](https://powersync.com) | ~38k | Adopt their sync backend |
87
+ | [`react-native-offline`](https://github.com/rgommezz/react-native-offline) | ~11k | Redux-coupled; **last published Feb 2023** |
88
+ | **this package** | — | Nothing. One function |
89
+
90
+ *Figures collected September 2026.*
91
+
92
+ **Be honest with yourself before installing this:**
93
+
94
+ - Already using **React Query**? Use its
95
+ [`persistQueryClient` with mutation resume](https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient).
96
+ It is excellent and you already have it.
97
+ - Already using **Redux**? `@redux-offline/redux-offline` is mature and
98
+ actively maintained.
99
+ - Need a **replicated local database**? WatermelonDB, RxDB or PowerSync.
100
+
101
+ **This package is for the case none of those fit:** no Redux, no React Query,
102
+ no local database — and no appetite for adopting one just so a `POST` survives
103
+ a tunnel. Zero runtime dependencies, one job.
104
+
105
+ There is a second reason it exists. `react-native-offline` still sees ~11k
106
+ downloads a week but has not shipped since **February 2023**. If you are on it
107
+ for the queue alone, this is a smaller, maintained, Redux-free replacement.
108
+
109
+ 📖 **[Full guide](docs/GUIDE.md)** — how it works, platform setup, permissions, idempotency and troubleshooting.
110
+
111
+ ## Install
112
+
113
+ ```sh
114
+ npm install react-native-outbox-mutation-queue
115
+ ```
116
+
117
+ No required runtime dependencies. Persistence and connectivity are injected,
118
+ so nothing is bundled that you might not use.
119
+
120
+ ## Quick start
121
+
122
+ ```ts
123
+ import { createQueue } from 'react-native-outbox-mutation-queue';
124
+ import AsyncStorage from '@react-native-async-storage/async-storage';
125
+ import NetInfo from '@react-native-community/netinfo';
126
+
127
+ export const queue = createQueue({
128
+ storage: AsyncStorage,
129
+
130
+ async execute(task) {
131
+ const res = await fetch(`https://api.example.com/${task.type}`, {
132
+ method: 'POST',
133
+ headers: { 'Content-Type': 'application/json' },
134
+ body: JSON.stringify(task.payload),
135
+ });
136
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
137
+ },
138
+
139
+ // 4xx will never succeed on retry — stop immediately.
140
+ classifyError(error) {
141
+ const status = Number(String(error).match(/HTTP (\d+)/)?.[1]);
142
+ return status >= 400 && status < 500 ? 'permanent' : 'transient';
143
+ },
144
+
145
+ // Retries exhausted, or a permanent failure. Reconcile here.
146
+ onDiscard(task, error) {
147
+ console.warn('Dropped', task.type, error);
148
+ },
149
+ });
150
+
151
+ NetInfo.addEventListener((state) => {
152
+ queue.setOnline(Boolean(state.isConnected));
153
+ });
154
+ ```
155
+
156
+ Then queue writes from anywhere:
157
+
158
+ ```ts
159
+ await queue.enqueue('updateProfile', { name: 'Ada' });
160
+ ```
161
+
162
+ ## Showing sync state
163
+
164
+ ```tsx
165
+ import { useOfflineQueue } from 'react-native-outbox-mutation-queue/react';
166
+
167
+ function SyncBadge() {
168
+ const { pending, isOnline } = useOfflineQueue(queue);
169
+ if (pending === 0) return null;
170
+ return (
171
+ <Text>
172
+ {pending} change{pending === 1 ? '' : 's'} waiting
173
+ {isOnline ? ' — syncing…' : ' — offline'}
174
+ </Text>
175
+ );
176
+ }
177
+ ```
178
+
179
+ ## Deduplication
180
+
181
+ A user editing a form offline generates a write per keystroke-save. You almost
182
+ never want to replay all of them.
183
+
184
+ ```ts
185
+ await queue.enqueue('saveDraft', { text: 'a' }, { dedupeKey: 'draft:42' });
186
+ await queue.enqueue('saveDraft', { text: 'ab' }, { dedupeKey: 'draft:42' });
187
+ await queue.enqueue('saveDraft', { text: 'abc' }, { dedupeKey: 'draft:42' });
188
+ // One task is queued, carrying { text: 'abc' }.
189
+ ```
190
+
191
+ | Strategy | Behaviour |
192
+ |---|---|
193
+ | `replace` *(default)* | Keep queue position, take the newest payload |
194
+ | `drop` | Keep the task already queued, ignore the new one |
195
+ | `keep` | Do not collapse — queue every task |
196
+
197
+ Only **pending** tasks collapse. A task already in flight is never replaced —
198
+ that would leave its side effect half-applied.
199
+
200
+ ⚠️ **Deduplication throws work away.** That is the point for drafts, and a
201
+ data-loss bug for anything that must each be delivered — chat messages above
202
+ all. Two safeguards:
203
+
204
+ - The queue emits **`deduped(kept, dropped, strategy)`** so you can observe it
205
+ - It logs a **one-time warning** the first time a collapse happens; pass
206
+ `silenceDedupeWarning: true` once you have confirmed it is intended
207
+
208
+ Tasks without a `dedupeKey` are never collapsed.
209
+
210
+ ## Retries
211
+
212
+ Exponential backoff with jitter, so a fleet of devices reconnecting together
213
+ does not stampede your API.
214
+
215
+ ```ts
216
+ retry: {
217
+ maxAttempts: 5,
218
+ baseDelayMs: 1000,
219
+ maxDelayMs: 60_000,
220
+ jitter: 0.3,
221
+ }
222
+ ```
223
+
224
+ Delays follow `base × 2^(n-1)`, clamped to `maxDelayMs`, then randomised
225
+ within ±`jitter`. Default schedule: **1s → 2s → 4s → 8s → 16s**.
226
+
227
+ ## API
228
+
229
+ ### `createQueue(config)`
230
+
231
+ | Option | Default | Purpose |
232
+ |---|---|---|
233
+ | `execute` | *required* | Performs the side effect. Throw to retry |
234
+ | `storage` | in-memory | Any AsyncStorage-shaped adapter |
235
+ | `storageKey` | `rn-offline-queue/v1` | Persistence key |
236
+ | `retry` | see above | Backoff policy |
237
+ | `dedupeStrategy` | `replace` | Default collapsing behaviour |
238
+ | `concurrency` | `1` | Parallel executions; `1` preserves order |
239
+ | `classifyError` | `transient` | Return `permanent` to stop retrying |
240
+ | `onDiscard` | — | Conflict hook: retries exhausted or permanent |
241
+ | `autoStart` | `true` | Begin processing immediately |
242
+ | `silenceDedupeWarning` | `false` | Suppress the one-time dedupe warning |
243
+
244
+ ### Methods
245
+
246
+ ```ts
247
+ queue.ready() // resolves once storage is hydrated
248
+ queue.enqueue(type, payload, opts) // add a mutation
249
+ queue.list() // snapshot, in execution order
250
+ queue.size()
251
+ queue.remove(id)
252
+ queue.clear()
253
+ queue.start() / queue.pause()
254
+ queue.setOnline(boolean)
255
+ queue.on(event, handler) // returns an unsubscribe function
256
+ ```
257
+
258
+ ### Events
259
+
260
+ `enqueued` · `deduped` · `started` · `succeeded` · `failed` · `discarded` ·
261
+ `drained` · `changed`
262
+
263
+ `deduped(kept, dropped, strategy)` fires whenever a task is collapsed, naming
264
+ the payload that was discarded — so the discard is observable rather than
265
+ silent.
266
+
267
+ ## Using it for chat
268
+
269
+ Sending messages is one of the best fits for this library — the WhatsApp
270
+ clock-icon behaviour. But chat needs different settings, and one default is
271
+ actively dangerous:
272
+
273
+ ⚠️ **Never use `dedupeKey` for messages.** Dedupe collapses tasks sharing a
274
+ key, which for chat means **deleted messages** — send three, two vanish.
275
+
276
+ ```ts
277
+ queue.enqueue('sendMessage', msg); // ✅ no dedupeKey
278
+ ```
279
+
280
+ Also raise `maxAttempts` (users expect messages to keep trying for hours),
281
+ keep `concurrency: 1` for ordering, and send a device-generated message id as
282
+ an idempotency key so retries do not post twice.
283
+
284
+ This only covers **sending**. Receiving still needs a WebSocket or push
285
+ notifications.
286
+
287
+ 📖 [Full chat recipe](docs/GUIDE.md#recipe-using-it-for-chat)
288
+
289
+ ## Behaviour worth knowing
290
+
291
+ **Ordering.** With the default `concurrency: 1`, tasks execute strictly in
292
+ enqueue order. Raise it only when your mutations are genuinely independent.
293
+
294
+ **Interrupted tasks.** If the process dies mid-flight, that task is restored
295
+ as `pending` and retried on next launch. Your `execute` should therefore be
296
+ **idempotent** — send an idempotency key if the API supports one.
297
+
298
+ **Storage failures are non-fatal.** If persistence throws, the in-memory queue
299
+ keeps working; you lose durability, not the queue.
300
+
301
+ **Corrupt state self-heals.** Unparseable stored data is discarded rather than
302
+ crashing on launch.
303
+
304
+ ## Custom storage
305
+
306
+ Anything with three methods works — MMKV, SQLite, a test double:
307
+
308
+ ```ts
309
+ import { MMKV } from 'react-native-mmkv';
310
+ const mmkv = new MMKV();
311
+
312
+ const storage = {
313
+ async getItem(k) { return mmkv.getString(k) ?? null; },
314
+ async setItem(k, v) { mmkv.set(k, v); },
315
+ async removeItem(k) { mmkv.delete(k); },
316
+ };
317
+ ```
318
+
319
+ ## Development
320
+
321
+ ```sh
322
+ npm install
323
+ npm test
324
+ npm run typecheck
325
+ npm run build
326
+ ```
327
+
328
+ ## Contributing
329
+
330
+ Issues and pull requests are welcome. Run `npm test` and `npm run typecheck`
331
+ before opening a PR.
332
+
333
+ ## Support this project
334
+
335
+ If this saved you an afternoon, you can say thanks:
336
+
337
+ <a href="https://www.buymeacoffee.com/arbab1" target="_blank">
338
+ <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="46" width="163">
339
+ </a>
340
+
341
+ Starring the repo helps too — it is how other developers find it.
342
+
343
+ ## License
344
+
345
+ MIT © [Syed Arbab Ali Shah](https://github.com/ARBAB1)
@@ -0,0 +1,12 @@
1
+ import type { RetryPolicy } from './types';
2
+ export declare const DEFAULT_RETRY: RetryPolicy;
3
+ /**
4
+ * Exponential backoff with full-width jitter.
5
+ *
6
+ * Delay grows as base * 2^(attempt-1), clamped to maxDelayMs, then has a
7
+ * random factor applied so a fleet of devices coming back online together
8
+ * does not retry in lockstep and stampede the API.
9
+ *
10
+ * @param attempt 1-based attempt number that just failed.
11
+ */
12
+ export declare function computeBackoff(attempt: number, policy?: RetryPolicy, random?: () => number): number;
package/lib/backoff.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_RETRY = void 0;
4
+ exports.computeBackoff = computeBackoff;
5
+ exports.DEFAULT_RETRY = {
6
+ maxAttempts: 5,
7
+ baseDelayMs: 1000,
8
+ maxDelayMs: 60000,
9
+ jitter: 0.3,
10
+ };
11
+ /**
12
+ * Exponential backoff with full-width jitter.
13
+ *
14
+ * Delay grows as base * 2^(attempt-1), clamped to maxDelayMs, then has a
15
+ * random factor applied so a fleet of devices coming back online together
16
+ * does not retry in lockstep and stampede the API.
17
+ *
18
+ * @param attempt 1-based attempt number that just failed.
19
+ */
20
+ function computeBackoff(attempt, policy = exports.DEFAULT_RETRY, random = Math.random) {
21
+ const exponent = Math.max(0, attempt - 1);
22
+ const raw = policy.baseDelayMs * 2 ** exponent;
23
+ const clamped = Math.min(raw, policy.maxDelayMs);
24
+ if (policy.jitter <= 0)
25
+ return Math.round(clamped);
26
+ // Spread within ±jitter of the clamped delay, never below zero.
27
+ const spread = clamped * policy.jitter;
28
+ const offset = (random() * 2 - 1) * spread;
29
+ return Math.max(0, Math.round(clamped + offset));
30
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { OfflineQueue, createQueue } from './queue';
2
+ export type { QueueConfig, EnqueueOptions } from './queue';
3
+ export { computeBackoff, DEFAULT_RETRY } from './backoff';
4
+ export { createMemoryStorage } from './storage/memory';
5
+ export type { StorageAdapter } from './storage/types';
6
+ export type { DedupeStrategy, FailureKind, QueueEventName, QueueEvents, RetryPolicy, Task, TaskStatus, Unsubscribe, } from './types';
package/lib/index.js ADDED
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMemoryStorage = exports.DEFAULT_RETRY = exports.computeBackoff = exports.createQueue = exports.OfflineQueue = void 0;
4
+ var queue_1 = require("./queue");
5
+ Object.defineProperty(exports, "OfflineQueue", { enumerable: true, get: function () { return queue_1.OfflineQueue; } });
6
+ Object.defineProperty(exports, "createQueue", { enumerable: true, get: function () { return queue_1.createQueue; } });
7
+ var backoff_1 = require("./backoff");
8
+ Object.defineProperty(exports, "computeBackoff", { enumerable: true, get: function () { return backoff_1.computeBackoff; } });
9
+ Object.defineProperty(exports, "DEFAULT_RETRY", { enumerable: true, get: function () { return backoff_1.DEFAULT_RETRY; } });
10
+ var memory_1 = require("./storage/memory");
11
+ Object.defineProperty(exports, "createMemoryStorage", { enumerable: true, get: function () { return memory_1.createMemoryStorage; } });
package/lib/queue.d.ts ADDED
@@ -0,0 +1,100 @@
1
+ import type { StorageAdapter } from './storage/types';
2
+ import type { DedupeStrategy, FailureKind, QueueEventName, QueueEvents, RetryPolicy, Task, Unsubscribe } from './types';
3
+ export interface EnqueueOptions {
4
+ /** Tasks sharing this key collapse per `dedupeStrategy`. */
5
+ dedupeKey?: string;
6
+ /** Overrides the queue-level strategy for this task only. */
7
+ dedupeStrategy?: DedupeStrategy;
8
+ }
9
+ export interface QueueConfig<P = unknown> {
10
+ /**
11
+ * Performs the actual side effect — usually an API call. Resolving marks
12
+ * the task done; throwing schedules a retry.
13
+ */
14
+ execute: (task: Task<P>) => Promise<void>;
15
+ /** Where tasks are persisted. Defaults to in-memory (non-durable). */
16
+ storage?: StorageAdapter;
17
+ /** Key under which the task list is stored. */
18
+ storageKey?: string;
19
+ retry?: Partial<RetryPolicy>;
20
+ /** Default collapsing behaviour for tasks carrying a `dedupeKey`. */
21
+ dedupeStrategy?: DedupeStrategy;
22
+ /** How many tasks may run at once. Default 1 (strict ordering). */
23
+ concurrency?: number;
24
+ /**
25
+ * Classifies a thrown error. Returning `permanent` stops retrying
26
+ * immediately — use it for 4xx responses that will never succeed.
27
+ */
28
+ classifyError?: (error: unknown, task: Task<P>) => FailureKind;
29
+ /**
30
+ * Called when a task exhausts its retries or fails permanently. This is
31
+ * the conflict hook: reconcile server state, surface a prompt, or drop it.
32
+ */
33
+ onDiscard?: (task: Task<P>, error: unknown) => void | Promise<void>;
34
+ /** Start processing as soon as the queue is constructed. Default true. */
35
+ autoStart?: boolean;
36
+ /**
37
+ * Suppress the one-time development warning emitted the first time a task
38
+ * is collapsed by `dedupeKey`. Set this once you have confirmed the
39
+ * collapsing is intended.
40
+ */
41
+ silenceDedupeWarning?: boolean;
42
+ }
43
+ export declare class OfflineQueue<P = unknown> {
44
+ private tasks;
45
+ private readonly storage;
46
+ private readonly storageKey;
47
+ private readonly retry;
48
+ private readonly dedupeStrategy;
49
+ private readonly concurrency;
50
+ private readonly config;
51
+ private readonly silenceDedupeWarning;
52
+ private warnedAboutDedupe;
53
+ private online;
54
+ private running;
55
+ private draining;
56
+ private inFlight;
57
+ private timer;
58
+ private hydrated;
59
+ private listeners;
60
+ constructor(config: QueueConfig<P>);
61
+ /** Resolves once persisted tasks have been loaded from storage. */
62
+ ready(): Promise<void>;
63
+ /** Resume processing. */
64
+ start(): void;
65
+ /** Pause processing. In-flight tasks are allowed to finish. */
66
+ pause(): void;
67
+ /**
68
+ * Report connectivity. Wire this to NetInfo — the queue holds tasks while
69
+ * offline rather than burning retry attempts against a dead network.
70
+ */
71
+ setOnline(online: boolean): void;
72
+ isOnline(): boolean;
73
+ /** Add a mutation to the queue. Safe to call while offline. */
74
+ enqueue(type: string, payload: P, options?: EnqueueOptions): Promise<Task<P>>;
75
+ /** Snapshot of queued tasks, in execution order. */
76
+ list(): Task<P>[];
77
+ size(): number;
78
+ /** Remove a single task without executing it. */
79
+ remove(id: string): Promise<boolean>;
80
+ /** Drop every queued task. */
81
+ clear(): Promise<void>;
82
+ on<K extends QueueEventName>(event: K, handler: QueueEvents<P>[K]): Unsubscribe;
83
+ /**
84
+ * Deduplication discards work by design, but a silent discard is how the
85
+ * "my chat messages disappeared" bug happens. Warn once per queue in dev so
86
+ * the behaviour is discovered during development rather than in production.
87
+ */
88
+ private noteDedupe;
89
+ private hydrate;
90
+ private persist;
91
+ private nextRunnable;
92
+ private drain;
93
+ /** Wake up when the earliest backed-off task becomes eligible. */
94
+ private scheduleNext;
95
+ private clearTimer;
96
+ private run;
97
+ private handleFailure;
98
+ private emit;
99
+ }
100
+ export declare function createQueue<P = unknown>(config: QueueConfig<P>): OfflineQueue<P>;