react-websocket 3.6.0 → 4.18.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 (52) hide show
  1. package/README.md +34 -5
  2. package/dist/ag-ui.cjs +421 -0
  3. package/dist/ag-ui.cjs.map +1 -0
  4. package/dist/ag-ui.d.cts +493 -0
  5. package/dist/ag-ui.d.ts +171 -0
  6. package/dist/ag-ui.js +418 -0
  7. package/dist/ag-ui.js.map +1 -0
  8. package/dist/core.cjs +727 -162
  9. package/dist/core.cjs.map +1 -1
  10. package/dist/core.d.cts +501 -62
  11. package/dist/core.d.ts +501 -62
  12. package/dist/core.js +723 -163
  13. package/dist/core.js.map +1 -1
  14. package/dist/devtools.cjs +186 -0
  15. package/dist/devtools.cjs.map +1 -0
  16. package/dist/devtools.d.cts +32 -0
  17. package/dist/devtools.d.ts +32 -0
  18. package/dist/devtools.js +185 -0
  19. package/dist/devtools.js.map +1 -0
  20. package/dist/index.cjs +214 -19
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +705 -56
  23. package/dist/index.d.ts +421 -30
  24. package/dist/index.js +211 -21
  25. package/dist/index.js.map +1 -1
  26. package/dist/outbox.cjs +222 -0
  27. package/dist/outbox.cjs.map +1 -0
  28. package/dist/outbox.d.cts +127 -0
  29. package/dist/outbox.d.ts +127 -0
  30. package/dist/outbox.js +219 -0
  31. package/dist/outbox.js.map +1 -0
  32. package/dist/replay.cjs +324 -0
  33. package/dist/replay.cjs.map +1 -0
  34. package/dist/replay.d.cts +124 -0
  35. package/dist/replay.d.ts +124 -0
  36. package/dist/replay.js +322 -0
  37. package/dist/replay.js.map +1 -0
  38. package/dist/tabs.cjs +336 -0
  39. package/dist/tabs.cjs.map +1 -0
  40. package/dist/tabs.d.cts +81 -0
  41. package/dist/tabs.d.ts +81 -0
  42. package/dist/tabs.js +335 -0
  43. package/dist/tabs.js.map +1 -0
  44. package/dist/timeline.d.cts +1 -213
  45. package/dist/types-BEecFqIB.d.cts +268 -0
  46. package/dist/webtransport.cjs +243 -0
  47. package/dist/webtransport.cjs.map +1 -0
  48. package/dist/webtransport.d.cts +67 -0
  49. package/dist/webtransport.d.ts +67 -0
  50. package/dist/webtransport.js +242 -0
  51. package/dist/webtransport.js.map +1 -0
  52. package/package.json +139 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox.cjs","names":[],"sources":["../src/outbox.ts"],"sourcesContent":["// Types only: the built entry has no imports, and its declarations point at `core`'s.\nimport type { WebSocketClient } from './core';\n\n/** What an outbox stores for each message. */\nexport interface OutboxRecord<M = unknown> {\n /** The message's idempotency key, sent with it every time. */\n readonly id: string;\n /** The message, as passed to `add()`. */\n readonly message: M;\n /** When it was added, from `Date.now()`. Entries are sent in this order. */\n readonly createdAt: number;\n}\n\n/** A message waiting in the outbox. */\nexport interface OutboxEntry<M = unknown> extends OutboxRecord<M> {\n /** Sent on the current connection, and waiting for the server's ack. */\n readonly sent: boolean;\n}\n\n/**\n * Where an outbox keeps its messages. The default, {@link indexedDBStorage}, keeps them across\n * reloads. Pass your own to store them elsewhere, or {@link memoryStorage} in tests.\n */\nexport interface OutboxStorage {\n /** Every stored record, in any order. */\n load(): Promise<OutboxRecord[]>;\n put(record: OutboxRecord): Promise<void>;\n delete(id: string): Promise<void>;\n}\n\n/** Options for {@link createWebSocketOutbox}. */\nexport interface WebSocketOutboxOptions<M, In, Out> {\n /**\n * Names the outbox's IndexedDB database, `react-websocket-outbox:<name>`. Give each outbox\n * its own. Unused with `storage`.\n */\n name: string;\n /**\n * Build the message to send for an entry, with its `id` as the idempotency key: the server\n * should do nothing for an id it has already handled, except acknowledge it again. Annotate\n * `message`: that's what `outbox.add()` accepts.\n */\n send: (message: M, id: string) => Out;\n /**\n * Recognize the server's acknowledgement and return the id it confirms, or `undefined` (or\n * `null`) for any other message. An entry stays until it's acknowledged, and is sent again\n * on every new connection until then. Without `ack`, an entry is removed once it has been\n * sent, so one sent just before the connection dropped can be lost.\n */\n ack?: (message: In) => string | null | undefined;\n /**\n * Drop an entry instead of sending it once it's older than this many ms. Checked when it\n * would be sent: on a new connection, and when stored entries are loaded.\n * @default Infinity\n */\n maxAge?: number;\n /** Called with each entry dropped by `maxAge`. */\n onExpire?: (entry: OutboxEntry<M>) => void;\n /**\n * Where to keep entries.\n * @default indexedDBStorage(name), or memoryStorage() where IndexedDB doesn't exist\n */\n storage?: OutboxStorage;\n}\n\n/** A persistent outbox on one client. Create with {@link createWebSocketOutbox}. */\nexport interface WebSocketOutbox<M> {\n /** The client the entries are sent on. */\n readonly client: WebSocketClient<any, any>;\n /**\n * Resolves once the entries stored by an earlier page load are back in the outbox, and\n * rejects if they couldn't be loaded. Nothing is sent before.\n */\n readonly ready: Promise<void>;\n /**\n * Add a message and send it as soon as the socket is open. Resolves with its id once it's\n * stored, and rejects if storing failed; the entry is still sent from memory then, but won't\n * survive a reload. Without `ack`, one sent right away isn't stored, and resolves at once.\n * @param id - The idempotency key, e.g. an order number. A new random one by default.\n */\n add(message: M, id?: string): Promise<string>;\n /**\n * Remove an entry without sending it again, e.g. when the user cancels it. Other tabs using\n * the same `name` drop it too.\n */\n remove(id: string): Promise<void>;\n /**\n * Remove every entry at once, including ones other tabs stored, e.g. on logout. Other tabs\n * using the same `name` drop theirs too. Resolves once they're deleted from storage.\n */\n clear(): Promise<void>;\n /** The waiting entries, oldest first. A new array on every change. */\n getSnapshot(): readonly OutboxEntry<M>[];\n /** Get notified when the entries change. Compatible with `useSyncExternalStore`. */\n subscribe(listener: () => void): () => void;\n /** Stop sending and listening, and release the connection. Stored entries stay stored. */\n dispose(): void;\n}\n\n/** Keep outbox entries in IndexedDB, in the database `react-websocket-outbox:<name>`. */\nexport function indexedDBStorage(name: string): OutboxStorage {\n let db: Promise<IDBDatabase> | undefined;\n // Settles once the transaction has committed, not when its request succeeds: a write isn't\n // durable before, and could still be lost if the page closed.\n const run = async <T>(\n mode: IDBTransactionMode,\n ask: (store: IDBObjectStore) => IDBRequest<T>\n ): Promise<T> => {\n db ??= new Promise((resolve, reject) => {\n const open = indexedDB.open(`react-websocket-outbox:${name}`, 1);\n open.onupgradeneeded = () => open.result.createObjectStore('entries', { keyPath: 'id' });\n open.onsuccess = () => {\n const connection = open.result;\n // Let another page delete or upgrade the database: the next write opens it again.\n connection.onversionchange = () => {\n connection.close();\n db = undefined;\n };\n resolve(connection);\n };\n open.onerror = () => reject(open.error);\n });\n const transaction = (await db).transaction('entries', mode);\n const req = ask(transaction.objectStore('entries'));\n return new Promise((resolve, reject) => {\n transaction.oncomplete = () => resolve(req.result);\n transaction.onerror = transaction.onabort = () => reject(transaction.error ?? req.error);\n });\n };\n return {\n load: () => run('readonly', store => store.getAll()),\n put: record => run('readwrite', store => store.put(record)).then(noop),\n delete: id => run('readwrite', store => store.delete(id))\n };\n}\n\n/** Keep outbox entries in memory only: they don't survive a reload. For tests and servers. */\nexport function memoryStorage(): OutboxStorage {\n const records = new Map<string, OutboxRecord>();\n return {\n load: async () => [...records.values()],\n put: async record => void records.set(record.id, record),\n delete: async id => void records.delete(id)\n };\n}\n\nconst noop = () => {};\n\nfunction randomId() {\n // `randomUUID` needs a secure context; plain-HTTP pages fall back to a timestamp and random digits.\n return (\n globalThis.crypto?.randomUUID?.() ??\n `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`\n );\n}\n\n/**\n * Keep messages until the server acknowledges them, across reconnects and page reloads, for\n * apps that must not lose a message on flaky Wi-Fi: point of sale, field service, warehouses.\n *\n * - `add(message)` stores the message (in IndexedDB by default) and sends it, tagged with an\n * idempotency key, as soon as the socket is open.\n * - It stays until the server acknowledges that key (`ack`). Every new connection sends the\n * unacknowledged entries again, in order, so the server must ignore a key it has already\n * handled and acknowledge it again.\n * - After a reload, the stored entries are loaded and sent before new ones.\n * - While entries are waiting, the outbox holds the client's connection open.\n *\n * Its entries don't go through the client's send queue, so `queue` options don't apply to them.\n *\n * @example\n * ```ts\n * import { createWebSocketOutbox } from 'react-websocket/outbox';\n *\n * export const orders = createWebSocketOutbox(pos, {\n * name: 'orders',\n * send: (order: Order, id) => ({ type: 'order', id, order }),\n * ack: message => (message.type === 'ack' ? message.id : undefined)\n * });\n *\n * await orders.add({ table: 4, items });\n * ```\n */\n// `const` + `NoInfer`: take `In` and `Out` from the client only; `M` comes from how `send`\n// annotates its parameter.\nexport function createWebSocketOutbox<M, In, const Out>(\n client: WebSocketClient<In, Out>,\n options: WebSocketOutboxOptions<M, NoInfer<In>, NoInfer<Out>>\n): WebSocketOutbox<M> {\n const storage =\n options.storage ??\n (typeof indexedDB === 'undefined' ? memoryStorage() : indexedDBStorage(options.name));\n const maxAge = options.maxAge ?? Infinity;\n // Insertion order is `createdAt` order: entries loaded from storage are sorted in.\n let entries = new Map<string, OutboxEntry<M>>();\n let snapshot: readonly OutboxEntry<M>[] = [];\n const listeners = new Set<() => void>();\n let loaded = false;\n // Removed (or acknowledged) before the stored entries were loaded: don't restore them.\n const removed = new Set<string>();\n // Entries created before this were cleared, here or in another tab: don't restore them.\n let clearedBefore = 0;\n // Other tabs using the same outbox, which loaded the same entries: tell them what's gone. Not\n // on a server, where an open channel would keep the process alive.\n const channel =\n typeof window !== 'undefined' && typeof BroadcastChannel === 'function'\n ? new BroadcastChannel(`react-websocket-outbox:${options.name}`)\n : undefined;\n let disposed = false;\n let release: (() => void) | undefined;\n // A strictly increasing `createdAt`, so entries added in the same ms keep their order.\n let lastCreated = 0;\n\n function changed() {\n snapshot = [...entries.values()];\n // Hold the connection while anything is waiting, so it gets delivered.\n if (snapshot.length && !release && !disposed) release = client.acquire();\n else if (!snapshot.length && release) {\n release();\n release = undefined;\n }\n for (const listener of [...listeners]) listener();\n }\n\n function drop(id: string) {\n if (!loaded) removed.add(id);\n if (entries.delete(id)) changed();\n }\n\n function dropBefore(before: number) {\n clearedBefore = Math.max(clearedBefore, before);\n let changes = false;\n for (const entry of entries.values()) {\n if (entry.createdAt < before) changes = entries.delete(entry.id);\n }\n if (changes) changed();\n }\n\n // Also deletes a stored entry that isn't loaded yet, so it doesn't come back.\n function forget(id: string) {\n drop(id);\n channel?.postMessage({ id });\n return storage.delete(id);\n }\n\n function flush() {\n // The socket, not the status, which reads `open` while the server closes it. Not `volatile`\n // either, which can be a datagram that never arrives.\n if (!loaded || disposed || client.socket?.readyState !== 1) return;\n const now = Date.now();\n let sent = false;\n for (const entry of [...entries.values()]) {\n if (entry.sent) continue;\n if (now - entry.createdAt > maxAge) {\n forget(entry.id).catch(noop);\n options.onExpire?.(entry);\n } else if (client.send(options.send(entry.message, entry.id))) {\n if (!options.ack) forget(entry.id).catch(noop);\n else {\n entries.set(entry.id, { ...entry, sent: true });\n sent = true;\n }\n }\n }\n if (sent) changed();\n }\n\n const ready = storage\n .load()\n .then(records => {\n if (disposed) return;\n // Entries added meanwhile are newer than the stored ones, unless one has the same id.\n entries = new Map(\n [\n ...records.filter(\n record =>\n record.createdAt >= clearedBefore &&\n !entries.has(record.id) &&\n !removed.has(record.id)\n ),\n ...entries.values()\n ]\n .sort((a, b) => a.createdAt - b.createdAt)\n .map(record => [record.id, { ...record, sent: false } as OutboxEntry<M>])\n );\n changed();\n })\n // Even without its stored entries, the outbox works from memory.\n .finally(() => {\n loaded = true;\n removed.clear();\n flush();\n });\n // Nobody may be awaiting `ready`; don't surface that as an unhandled rejection.\n ready.catch(noop);\n\n /** What was sent on the last connection may not have arrived: send it again on the next. */\n function unsend() {\n let changes = false;\n for (const entry of entries.values()) {\n if (entry.sent) {\n entries.set(entry.id, { ...entry, sent: false });\n changes = true;\n }\n }\n if (changes) changed();\n }\n\n if (channel) {\n channel.onmessage = ({ data }: MessageEvent<{ id?: string; before?: number } | null>) => {\n if (data?.before) dropBefore(data.before);\n else if (data?.id) drop(data.id);\n };\n }\n\n const offs = [\n () => channel?.close(),\n // Also after `migrate()`, where the status stays `open`.\n client.on('open', () => {\n unsend();\n flush();\n }),\n client.subscribe(() => {\n if (client.getSnapshot().status !== 'open') unsend();\n }),\n client.on('message', message => {\n const id = options.ack?.(message);\n if (id != null) forget(String(id)).catch(noop);\n })\n ];\n\n return {\n client,\n ready,\n add(message, id = randomId()) {\n lastCreated = Math.max(Date.now(), lastCreated + 1);\n const record: OutboxRecord<M> = { id, message, createdAt: lastCreated };\n // Re-adding an id moves it to the end, as a new message.\n entries.delete(id);\n entries.set(id, { ...record, sent: false });\n changed();\n flush();\n // Sent, with no ack to wait for: there's nothing to keep. Storing it now would outlive its\n // delete, and send it again after a reload.\n if (!entries.has(id)) return Promise.resolve(id);\n return storage.put(record).then(() => id);\n },\n remove: forget,\n clear() {\n // At once, like `remove()`: waiting for the stored entries would send them first.\n const before = (lastCreated = Math.max(Date.now(), lastCreated + 1));\n dropBefore(before);\n channel?.postMessage({ before });\n // Including what other tabs stored, but not what's added from now on.\n return storage\n .load()\n .then(records =>\n Promise.all(\n records\n .filter(record => record.createdAt < before)\n .map(record => storage.delete(record.id))\n )\n )\n .then(noop);\n },\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n dispose() {\n disposed = true;\n for (const off of offs) off();\n release?.();\n release = undefined;\n }\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,127 @@
1
+ import { t as WebSocketClient } from "./types-BEecFqIB.cjs";
2
+ //#region src/outbox.d.ts
3
+ /** What an outbox stores for each message. */
4
+ export interface OutboxRecord<M = unknown> {
5
+ /** The message's idempotency key, sent with it every time. */
6
+ readonly id: string;
7
+ /** The message, as passed to `add()`. */
8
+ readonly message: M;
9
+ /** When it was added, from `Date.now()`. Entries are sent in this order. */
10
+ readonly createdAt: number;
11
+ }
12
+ /** A message waiting in the outbox. */
13
+ export interface OutboxEntry<M = unknown> extends OutboxRecord<M> {
14
+ /** Sent on the current connection, and waiting for the server's ack. */
15
+ readonly sent: boolean;
16
+ }
17
+ /**
18
+ * Where an outbox keeps its messages. The default, {@link indexedDBStorage}, keeps them across
19
+ * reloads. Pass your own to store them elsewhere, or {@link memoryStorage} in tests.
20
+ */
21
+ export interface OutboxStorage {
22
+ /** Every stored record, in any order. */
23
+ load(): Promise<OutboxRecord[]>;
24
+ put(record: OutboxRecord): Promise<void>;
25
+ delete(id: string): Promise<void>;
26
+ }
27
+ /** Options for {@link createWebSocketOutbox}. */
28
+ export interface WebSocketOutboxOptions<M, In, Out> {
29
+ /**
30
+ * Names the outbox's IndexedDB database, `react-websocket-outbox:<name>`. Give each outbox
31
+ * its own. Unused with `storage`.
32
+ */
33
+ name: string;
34
+ /**
35
+ * Build the message to send for an entry, with its `id` as the idempotency key: the server
36
+ * should do nothing for an id it has already handled, except acknowledge it again. Annotate
37
+ * `message`: that's what `outbox.add()` accepts.
38
+ */
39
+ send: (message: M, id: string) => Out;
40
+ /**
41
+ * Recognize the server's acknowledgement and return the id it confirms, or `undefined` (or
42
+ * `null`) for any other message. An entry stays until it's acknowledged, and is sent again
43
+ * on every new connection until then. Without `ack`, an entry is removed once it has been
44
+ * sent, so one sent just before the connection dropped can be lost.
45
+ */
46
+ ack?: (message: In) => string | null | undefined;
47
+ /**
48
+ * Drop an entry instead of sending it once it's older than this many ms. Checked when it
49
+ * would be sent: on a new connection, and when stored entries are loaded.
50
+ * @default Infinity
51
+ */
52
+ maxAge?: number;
53
+ /** Called with each entry dropped by `maxAge`. */
54
+ onExpire?: (entry: OutboxEntry<M>) => void;
55
+ /**
56
+ * Where to keep entries.
57
+ * @default indexedDBStorage(name), or memoryStorage() where IndexedDB doesn't exist
58
+ */
59
+ storage?: OutboxStorage;
60
+ }
61
+ /** A persistent outbox on one client. Create with {@link createWebSocketOutbox}. */
62
+ export interface WebSocketOutbox<M> {
63
+ /** The client the entries are sent on. */
64
+ readonly client: WebSocketClient<any, any>;
65
+ /**
66
+ * Resolves once the entries stored by an earlier page load are back in the outbox, and
67
+ * rejects if they couldn't be loaded. Nothing is sent before.
68
+ */
69
+ readonly ready: Promise<void>;
70
+ /**
71
+ * Add a message and send it as soon as the socket is open. Resolves with its id once it's
72
+ * stored, and rejects if storing failed; the entry is still sent from memory then, but won't
73
+ * survive a reload. Without `ack`, one sent right away isn't stored, and resolves at once.
74
+ * @param id - The idempotency key, e.g. an order number. A new random one by default.
75
+ */
76
+ add(message: M, id?: string): Promise<string>;
77
+ /**
78
+ * Remove an entry without sending it again, e.g. when the user cancels it. Other tabs using
79
+ * the same `name` drop it too.
80
+ */
81
+ remove(id: string): Promise<void>;
82
+ /**
83
+ * Remove every entry at once, including ones other tabs stored, e.g. on logout. Other tabs
84
+ * using the same `name` drop theirs too. Resolves once they're deleted from storage.
85
+ */
86
+ clear(): Promise<void>;
87
+ /** The waiting entries, oldest first. A new array on every change. */
88
+ getSnapshot(): readonly OutboxEntry<M>[];
89
+ /** Get notified when the entries change. Compatible with `useSyncExternalStore`. */
90
+ subscribe(listener: () => void): () => void;
91
+ /** Stop sending and listening, and release the connection. Stored entries stay stored. */
92
+ dispose(): void;
93
+ }
94
+ /** Keep outbox entries in IndexedDB, in the database `react-websocket-outbox:<name>`. */
95
+ export declare function indexedDBStorage(name: string): OutboxStorage;
96
+ /** Keep outbox entries in memory only: they don't survive a reload. For tests and servers. */
97
+ export declare function memoryStorage(): OutboxStorage;
98
+ /**
99
+ * Keep messages until the server acknowledges them, across reconnects and page reloads, for
100
+ * apps that must not lose a message on flaky Wi-Fi: point of sale, field service, warehouses.
101
+ *
102
+ * - `add(message)` stores the message (in IndexedDB by default) and sends it, tagged with an
103
+ * idempotency key, as soon as the socket is open.
104
+ * - It stays until the server acknowledges that key (`ack`). Every new connection sends the
105
+ * unacknowledged entries again, in order, so the server must ignore a key it has already
106
+ * handled and acknowledge it again.
107
+ * - After a reload, the stored entries are loaded and sent before new ones.
108
+ * - While entries are waiting, the outbox holds the client's connection open.
109
+ *
110
+ * Its entries don't go through the client's send queue, so `queue` options don't apply to them.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * import { createWebSocketOutbox } from 'react-websocket/outbox';
115
+ *
116
+ * export const orders = createWebSocketOutbox(pos, {
117
+ * name: 'orders',
118
+ * send: (order: Order, id) => ({ type: 'order', id, order }),
119
+ * ack: message => (message.type === 'ack' ? message.id : undefined)
120
+ * });
121
+ *
122
+ * await orders.add({ table: 4, items });
123
+ * ```
124
+ */
125
+ export declare function createWebSocketOutbox<M, In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketOutboxOptions<M, NoInfer<In>, NoInfer<Out>>): WebSocketOutbox<M>;
126
+ //#endregion
127
+ //# sourceMappingURL=outbox.d.cts.map
@@ -0,0 +1,127 @@
1
+ import { WebSocketClient } from "./core.js";
2
+ //#region src/outbox.d.ts
3
+ /** What an outbox stores for each message. */
4
+ export interface OutboxRecord<M = unknown> {
5
+ /** The message's idempotency key, sent with it every time. */
6
+ readonly id: string;
7
+ /** The message, as passed to `add()`. */
8
+ readonly message: M;
9
+ /** When it was added, from `Date.now()`. Entries are sent in this order. */
10
+ readonly createdAt: number;
11
+ }
12
+ /** A message waiting in the outbox. */
13
+ export interface OutboxEntry<M = unknown> extends OutboxRecord<M> {
14
+ /** Sent on the current connection, and waiting for the server's ack. */
15
+ readonly sent: boolean;
16
+ }
17
+ /**
18
+ * Where an outbox keeps its messages. The default, {@link indexedDBStorage}, keeps them across
19
+ * reloads. Pass your own to store them elsewhere, or {@link memoryStorage} in tests.
20
+ */
21
+ export interface OutboxStorage {
22
+ /** Every stored record, in any order. */
23
+ load(): Promise<OutboxRecord[]>;
24
+ put(record: OutboxRecord): Promise<void>;
25
+ delete(id: string): Promise<void>;
26
+ }
27
+ /** Options for {@link createWebSocketOutbox}. */
28
+ export interface WebSocketOutboxOptions<M, In, Out> {
29
+ /**
30
+ * Names the outbox's IndexedDB database, `react-websocket-outbox:<name>`. Give each outbox
31
+ * its own. Unused with `storage`.
32
+ */
33
+ name: string;
34
+ /**
35
+ * Build the message to send for an entry, with its `id` as the idempotency key: the server
36
+ * should do nothing for an id it has already handled, except acknowledge it again. Annotate
37
+ * `message`: that's what `outbox.add()` accepts.
38
+ */
39
+ send: (message: M, id: string) => Out;
40
+ /**
41
+ * Recognize the server's acknowledgement and return the id it confirms, or `undefined` (or
42
+ * `null`) for any other message. An entry stays until it's acknowledged, and is sent again
43
+ * on every new connection until then. Without `ack`, an entry is removed once it has been
44
+ * sent, so one sent just before the connection dropped can be lost.
45
+ */
46
+ ack?: (message: In) => string | null | undefined;
47
+ /**
48
+ * Drop an entry instead of sending it once it's older than this many ms. Checked when it
49
+ * would be sent: on a new connection, and when stored entries are loaded.
50
+ * @default Infinity
51
+ */
52
+ maxAge?: number;
53
+ /** Called with each entry dropped by `maxAge`. */
54
+ onExpire?: (entry: OutboxEntry<M>) => void;
55
+ /**
56
+ * Where to keep entries.
57
+ * @default indexedDBStorage(name), or memoryStorage() where IndexedDB doesn't exist
58
+ */
59
+ storage?: OutboxStorage;
60
+ }
61
+ /** A persistent outbox on one client. Create with {@link createWebSocketOutbox}. */
62
+ export interface WebSocketOutbox<M> {
63
+ /** The client the entries are sent on. */
64
+ readonly client: WebSocketClient<any, any>;
65
+ /**
66
+ * Resolves once the entries stored by an earlier page load are back in the outbox, and
67
+ * rejects if they couldn't be loaded. Nothing is sent before.
68
+ */
69
+ readonly ready: Promise<void>;
70
+ /**
71
+ * Add a message and send it as soon as the socket is open. Resolves with its id once it's
72
+ * stored, and rejects if storing failed; the entry is still sent from memory then, but won't
73
+ * survive a reload. Without `ack`, one sent right away isn't stored, and resolves at once.
74
+ * @param id - The idempotency key, e.g. an order number. A new random one by default.
75
+ */
76
+ add(message: M, id?: string): Promise<string>;
77
+ /**
78
+ * Remove an entry without sending it again, e.g. when the user cancels it. Other tabs using
79
+ * the same `name` drop it too.
80
+ */
81
+ remove(id: string): Promise<void>;
82
+ /**
83
+ * Remove every entry at once, including ones other tabs stored, e.g. on logout. Other tabs
84
+ * using the same `name` drop theirs too. Resolves once they're deleted from storage.
85
+ */
86
+ clear(): Promise<void>;
87
+ /** The waiting entries, oldest first. A new array on every change. */
88
+ getSnapshot(): readonly OutboxEntry<M>[];
89
+ /** Get notified when the entries change. Compatible with `useSyncExternalStore`. */
90
+ subscribe(listener: () => void): () => void;
91
+ /** Stop sending and listening, and release the connection. Stored entries stay stored. */
92
+ dispose(): void;
93
+ }
94
+ /** Keep outbox entries in IndexedDB, in the database `react-websocket-outbox:<name>`. */
95
+ export declare function indexedDBStorage(name: string): OutboxStorage;
96
+ /** Keep outbox entries in memory only: they don't survive a reload. For tests and servers. */
97
+ export declare function memoryStorage(): OutboxStorage;
98
+ /**
99
+ * Keep messages until the server acknowledges them, across reconnects and page reloads, for
100
+ * apps that must not lose a message on flaky Wi-Fi: point of sale, field service, warehouses.
101
+ *
102
+ * - `add(message)` stores the message (in IndexedDB by default) and sends it, tagged with an
103
+ * idempotency key, as soon as the socket is open.
104
+ * - It stays until the server acknowledges that key (`ack`). Every new connection sends the
105
+ * unacknowledged entries again, in order, so the server must ignore a key it has already
106
+ * handled and acknowledge it again.
107
+ * - After a reload, the stored entries are loaded and sent before new ones.
108
+ * - While entries are waiting, the outbox holds the client's connection open.
109
+ *
110
+ * Its entries don't go through the client's send queue, so `queue` options don't apply to them.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * import { createWebSocketOutbox } from 'react-websocket/outbox';
115
+ *
116
+ * export const orders = createWebSocketOutbox(pos, {
117
+ * name: 'orders',
118
+ * send: (order: Order, id) => ({ type: 'order', id, order }),
119
+ * ack: message => (message.type === 'ack' ? message.id : undefined)
120
+ * });
121
+ *
122
+ * await orders.add({ table: 4, items });
123
+ * ```
124
+ */
125
+ export declare function createWebSocketOutbox<M, In, const Out>(client: WebSocketClient<In, Out>, options: WebSocketOutboxOptions<M, NoInfer<In>, NoInfer<Out>>): WebSocketOutbox<M>;
126
+ //#endregion
127
+ //# sourceMappingURL=outbox.d.ts.map
package/dist/outbox.js ADDED
@@ -0,0 +1,219 @@
1
+ //#region src/outbox.ts
2
+ /** Keep outbox entries in IndexedDB, in the database `react-websocket-outbox:<name>`. */
3
+ function indexedDBStorage(name) {
4
+ let db;
5
+ const run = async (mode, ask) => {
6
+ db ??= new Promise((resolve, reject) => {
7
+ const open = indexedDB.open(`react-websocket-outbox:${name}`, 1);
8
+ open.onupgradeneeded = () => open.result.createObjectStore("entries", { keyPath: "id" });
9
+ open.onsuccess = () => {
10
+ const connection = open.result;
11
+ connection.onversionchange = () => {
12
+ connection.close();
13
+ db = void 0;
14
+ };
15
+ resolve(connection);
16
+ };
17
+ open.onerror = () => reject(open.error);
18
+ });
19
+ const transaction = (await db).transaction("entries", mode);
20
+ const req = ask(transaction.objectStore("entries"));
21
+ return new Promise((resolve, reject) => {
22
+ transaction.oncomplete = () => resolve(req.result);
23
+ transaction.onerror = transaction.onabort = () => reject(transaction.error ?? req.error);
24
+ });
25
+ };
26
+ return {
27
+ load: () => run("readonly", (store) => store.getAll()),
28
+ put: (record) => run("readwrite", (store) => store.put(record)).then(noop),
29
+ delete: (id) => run("readwrite", (store) => store.delete(id))
30
+ };
31
+ }
32
+ /** Keep outbox entries in memory only: they don't survive a reload. For tests and servers. */
33
+ function memoryStorage() {
34
+ const records = /* @__PURE__ */ new Map();
35
+ return {
36
+ load: async () => [...records.values()],
37
+ put: async (record) => void records.set(record.id, record),
38
+ delete: async (id) => void records.delete(id)
39
+ };
40
+ }
41
+ const noop = () => {};
42
+ function randomId() {
43
+ return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
44
+ }
45
+ /**
46
+ * Keep messages until the server acknowledges them, across reconnects and page reloads, for
47
+ * apps that must not lose a message on flaky Wi-Fi: point of sale, field service, warehouses.
48
+ *
49
+ * - `add(message)` stores the message (in IndexedDB by default) and sends it, tagged with an
50
+ * idempotency key, as soon as the socket is open.
51
+ * - It stays until the server acknowledges that key (`ack`). Every new connection sends the
52
+ * unacknowledged entries again, in order, so the server must ignore a key it has already
53
+ * handled and acknowledge it again.
54
+ * - After a reload, the stored entries are loaded and sent before new ones.
55
+ * - While entries are waiting, the outbox holds the client's connection open.
56
+ *
57
+ * Its entries don't go through the client's send queue, so `queue` options don't apply to them.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * import { createWebSocketOutbox } from 'react-websocket/outbox';
62
+ *
63
+ * export const orders = createWebSocketOutbox(pos, {
64
+ * name: 'orders',
65
+ * send: (order: Order, id) => ({ type: 'order', id, order }),
66
+ * ack: message => (message.type === 'ack' ? message.id : undefined)
67
+ * });
68
+ *
69
+ * await orders.add({ table: 4, items });
70
+ * ```
71
+ */
72
+ function createWebSocketOutbox(client, options) {
73
+ const storage = options.storage ?? (typeof indexedDB === "undefined" ? memoryStorage() : indexedDBStorage(options.name));
74
+ const maxAge = options.maxAge ?? Infinity;
75
+ let entries = /* @__PURE__ */ new Map();
76
+ let snapshot = [];
77
+ const listeners = /* @__PURE__ */ new Set();
78
+ let loaded = false;
79
+ const removed = /* @__PURE__ */ new Set();
80
+ let clearedBefore = 0;
81
+ const channel = typeof window !== "undefined" && typeof BroadcastChannel === "function" ? new BroadcastChannel(`react-websocket-outbox:${options.name}`) : void 0;
82
+ let disposed = false;
83
+ let release;
84
+ let lastCreated = 0;
85
+ function changed() {
86
+ snapshot = [...entries.values()];
87
+ if (snapshot.length && !release && !disposed) release = client.acquire();
88
+ else if (!snapshot.length && release) {
89
+ release();
90
+ release = void 0;
91
+ }
92
+ for (const listener of [...listeners]) listener();
93
+ }
94
+ function drop(id) {
95
+ if (!loaded) removed.add(id);
96
+ if (entries.delete(id)) changed();
97
+ }
98
+ function dropBefore(before) {
99
+ clearedBefore = Math.max(clearedBefore, before);
100
+ let changes = false;
101
+ for (const entry of entries.values()) if (entry.createdAt < before) changes = entries.delete(entry.id);
102
+ if (changes) changed();
103
+ }
104
+ function forget(id) {
105
+ drop(id);
106
+ channel?.postMessage({ id });
107
+ return storage.delete(id);
108
+ }
109
+ function flush() {
110
+ if (!loaded || disposed || client.socket?.readyState !== 1) return;
111
+ const now = Date.now();
112
+ let sent = false;
113
+ for (const entry of [...entries.values()]) {
114
+ if (entry.sent) continue;
115
+ if (now - entry.createdAt > maxAge) {
116
+ forget(entry.id).catch(noop);
117
+ options.onExpire?.(entry);
118
+ } else if (client.send(options.send(entry.message, entry.id))) {
119
+ if (!options.ack) forget(entry.id).catch(noop);
120
+ else {
121
+ entries.set(entry.id, {
122
+ ...entry,
123
+ sent: true
124
+ });
125
+ sent = true;
126
+ }
127
+ }
128
+ }
129
+ if (sent) changed();
130
+ }
131
+ const ready = storage.load().then((records) => {
132
+ if (disposed) return;
133
+ entries = new Map([...records.filter((record) => record.createdAt >= clearedBefore && !entries.has(record.id) && !removed.has(record.id)), ...entries.values()].sort((a, b) => a.createdAt - b.createdAt).map((record) => [record.id, {
134
+ ...record,
135
+ sent: false
136
+ }]));
137
+ changed();
138
+ }).finally(() => {
139
+ loaded = true;
140
+ removed.clear();
141
+ flush();
142
+ });
143
+ ready.catch(noop);
144
+ /** What was sent on the last connection may not have arrived: send it again on the next. */
145
+ function unsend() {
146
+ let changes = false;
147
+ for (const entry of entries.values()) if (entry.sent) {
148
+ entries.set(entry.id, {
149
+ ...entry,
150
+ sent: false
151
+ });
152
+ changes = true;
153
+ }
154
+ if (changes) changed();
155
+ }
156
+ if (channel) channel.onmessage = ({ data }) => {
157
+ if (data?.before) dropBefore(data.before);
158
+ else if (data?.id) drop(data.id);
159
+ };
160
+ const offs = [
161
+ () => channel?.close(),
162
+ client.on("open", () => {
163
+ unsend();
164
+ flush();
165
+ }),
166
+ client.subscribe(() => {
167
+ if (client.getSnapshot().status !== "open") unsend();
168
+ }),
169
+ client.on("message", (message) => {
170
+ const id = options.ack?.(message);
171
+ if (id != null) forget(String(id)).catch(noop);
172
+ })
173
+ ];
174
+ return {
175
+ client,
176
+ ready,
177
+ add(message, id = randomId()) {
178
+ lastCreated = Math.max(Date.now(), lastCreated + 1);
179
+ const record = {
180
+ id,
181
+ message,
182
+ createdAt: lastCreated
183
+ };
184
+ entries.delete(id);
185
+ entries.set(id, {
186
+ ...record,
187
+ sent: false
188
+ });
189
+ changed();
190
+ flush();
191
+ if (!entries.has(id)) return Promise.resolve(id);
192
+ return storage.put(record).then(() => id);
193
+ },
194
+ remove: forget,
195
+ clear() {
196
+ const before = lastCreated = Math.max(Date.now(), lastCreated + 1);
197
+ dropBefore(before);
198
+ channel?.postMessage({ before });
199
+ return storage.load().then((records) => Promise.all(records.filter((record) => record.createdAt < before).map((record) => storage.delete(record.id)))).then(noop);
200
+ },
201
+ getSnapshot: () => snapshot,
202
+ subscribe(listener) {
203
+ listeners.add(listener);
204
+ return () => {
205
+ listeners.delete(listener);
206
+ };
207
+ },
208
+ dispose() {
209
+ disposed = true;
210
+ for (const off of offs) off();
211
+ release?.();
212
+ release = void 0;
213
+ }
214
+ };
215
+ }
216
+ //#endregion
217
+ export { createWebSocketOutbox, indexedDBStorage, memoryStorage };
218
+
219
+ //# sourceMappingURL=outbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox.js","names":[],"sources":["../src/outbox.ts"],"sourcesContent":["// Types only: the built entry has no imports, and its declarations point at `core`'s.\nimport type { WebSocketClient } from './core';\n\n/** What an outbox stores for each message. */\nexport interface OutboxRecord<M = unknown> {\n /** The message's idempotency key, sent with it every time. */\n readonly id: string;\n /** The message, as passed to `add()`. */\n readonly message: M;\n /** When it was added, from `Date.now()`. Entries are sent in this order. */\n readonly createdAt: number;\n}\n\n/** A message waiting in the outbox. */\nexport interface OutboxEntry<M = unknown> extends OutboxRecord<M> {\n /** Sent on the current connection, and waiting for the server's ack. */\n readonly sent: boolean;\n}\n\n/**\n * Where an outbox keeps its messages. The default, {@link indexedDBStorage}, keeps them across\n * reloads. Pass your own to store them elsewhere, or {@link memoryStorage} in tests.\n */\nexport interface OutboxStorage {\n /** Every stored record, in any order. */\n load(): Promise<OutboxRecord[]>;\n put(record: OutboxRecord): Promise<void>;\n delete(id: string): Promise<void>;\n}\n\n/** Options for {@link createWebSocketOutbox}. */\nexport interface WebSocketOutboxOptions<M, In, Out> {\n /**\n * Names the outbox's IndexedDB database, `react-websocket-outbox:<name>`. Give each outbox\n * its own. Unused with `storage`.\n */\n name: string;\n /**\n * Build the message to send for an entry, with its `id` as the idempotency key: the server\n * should do nothing for an id it has already handled, except acknowledge it again. Annotate\n * `message`: that's what `outbox.add()` accepts.\n */\n send: (message: M, id: string) => Out;\n /**\n * Recognize the server's acknowledgement and return the id it confirms, or `undefined` (or\n * `null`) for any other message. An entry stays until it's acknowledged, and is sent again\n * on every new connection until then. Without `ack`, an entry is removed once it has been\n * sent, so one sent just before the connection dropped can be lost.\n */\n ack?: (message: In) => string | null | undefined;\n /**\n * Drop an entry instead of sending it once it's older than this many ms. Checked when it\n * would be sent: on a new connection, and when stored entries are loaded.\n * @default Infinity\n */\n maxAge?: number;\n /** Called with each entry dropped by `maxAge`. */\n onExpire?: (entry: OutboxEntry<M>) => void;\n /**\n * Where to keep entries.\n * @default indexedDBStorage(name), or memoryStorage() where IndexedDB doesn't exist\n */\n storage?: OutboxStorage;\n}\n\n/** A persistent outbox on one client. Create with {@link createWebSocketOutbox}. */\nexport interface WebSocketOutbox<M> {\n /** The client the entries are sent on. */\n readonly client: WebSocketClient<any, any>;\n /**\n * Resolves once the entries stored by an earlier page load are back in the outbox, and\n * rejects if they couldn't be loaded. Nothing is sent before.\n */\n readonly ready: Promise<void>;\n /**\n * Add a message and send it as soon as the socket is open. Resolves with its id once it's\n * stored, and rejects if storing failed; the entry is still sent from memory then, but won't\n * survive a reload. Without `ack`, one sent right away isn't stored, and resolves at once.\n * @param id - The idempotency key, e.g. an order number. A new random one by default.\n */\n add(message: M, id?: string): Promise<string>;\n /**\n * Remove an entry without sending it again, e.g. when the user cancels it. Other tabs using\n * the same `name` drop it too.\n */\n remove(id: string): Promise<void>;\n /**\n * Remove every entry at once, including ones other tabs stored, e.g. on logout. Other tabs\n * using the same `name` drop theirs too. Resolves once they're deleted from storage.\n */\n clear(): Promise<void>;\n /** The waiting entries, oldest first. A new array on every change. */\n getSnapshot(): readonly OutboxEntry<M>[];\n /** Get notified when the entries change. Compatible with `useSyncExternalStore`. */\n subscribe(listener: () => void): () => void;\n /** Stop sending and listening, and release the connection. Stored entries stay stored. */\n dispose(): void;\n}\n\n/** Keep outbox entries in IndexedDB, in the database `react-websocket-outbox:<name>`. */\nexport function indexedDBStorage(name: string): OutboxStorage {\n let db: Promise<IDBDatabase> | undefined;\n // Settles once the transaction has committed, not when its request succeeds: a write isn't\n // durable before, and could still be lost if the page closed.\n const run = async <T>(\n mode: IDBTransactionMode,\n ask: (store: IDBObjectStore) => IDBRequest<T>\n ): Promise<T> => {\n db ??= new Promise((resolve, reject) => {\n const open = indexedDB.open(`react-websocket-outbox:${name}`, 1);\n open.onupgradeneeded = () => open.result.createObjectStore('entries', { keyPath: 'id' });\n open.onsuccess = () => {\n const connection = open.result;\n // Let another page delete or upgrade the database: the next write opens it again.\n connection.onversionchange = () => {\n connection.close();\n db = undefined;\n };\n resolve(connection);\n };\n open.onerror = () => reject(open.error);\n });\n const transaction = (await db).transaction('entries', mode);\n const req = ask(transaction.objectStore('entries'));\n return new Promise((resolve, reject) => {\n transaction.oncomplete = () => resolve(req.result);\n transaction.onerror = transaction.onabort = () => reject(transaction.error ?? req.error);\n });\n };\n return {\n load: () => run('readonly', store => store.getAll()),\n put: record => run('readwrite', store => store.put(record)).then(noop),\n delete: id => run('readwrite', store => store.delete(id))\n };\n}\n\n/** Keep outbox entries in memory only: they don't survive a reload. For tests and servers. */\nexport function memoryStorage(): OutboxStorage {\n const records = new Map<string, OutboxRecord>();\n return {\n load: async () => [...records.values()],\n put: async record => void records.set(record.id, record),\n delete: async id => void records.delete(id)\n };\n}\n\nconst noop = () => {};\n\nfunction randomId() {\n // `randomUUID` needs a secure context; plain-HTTP pages fall back to a timestamp and random digits.\n return (\n globalThis.crypto?.randomUUID?.() ??\n `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`\n );\n}\n\n/**\n * Keep messages until the server acknowledges them, across reconnects and page reloads, for\n * apps that must not lose a message on flaky Wi-Fi: point of sale, field service, warehouses.\n *\n * - `add(message)` stores the message (in IndexedDB by default) and sends it, tagged with an\n * idempotency key, as soon as the socket is open.\n * - It stays until the server acknowledges that key (`ack`). Every new connection sends the\n * unacknowledged entries again, in order, so the server must ignore a key it has already\n * handled and acknowledge it again.\n * - After a reload, the stored entries are loaded and sent before new ones.\n * - While entries are waiting, the outbox holds the client's connection open.\n *\n * Its entries don't go through the client's send queue, so `queue` options don't apply to them.\n *\n * @example\n * ```ts\n * import { createWebSocketOutbox } from 'react-websocket/outbox';\n *\n * export const orders = createWebSocketOutbox(pos, {\n * name: 'orders',\n * send: (order: Order, id) => ({ type: 'order', id, order }),\n * ack: message => (message.type === 'ack' ? message.id : undefined)\n * });\n *\n * await orders.add({ table: 4, items });\n * ```\n */\n// `const` + `NoInfer`: take `In` and `Out` from the client only; `M` comes from how `send`\n// annotates its parameter.\nexport function createWebSocketOutbox<M, In, const Out>(\n client: WebSocketClient<In, Out>,\n options: WebSocketOutboxOptions<M, NoInfer<In>, NoInfer<Out>>\n): WebSocketOutbox<M> {\n const storage =\n options.storage ??\n (typeof indexedDB === 'undefined' ? memoryStorage() : indexedDBStorage(options.name));\n const maxAge = options.maxAge ?? Infinity;\n // Insertion order is `createdAt` order: entries loaded from storage are sorted in.\n let entries = new Map<string, OutboxEntry<M>>();\n let snapshot: readonly OutboxEntry<M>[] = [];\n const listeners = new Set<() => void>();\n let loaded = false;\n // Removed (or acknowledged) before the stored entries were loaded: don't restore them.\n const removed = new Set<string>();\n // Entries created before this were cleared, here or in another tab: don't restore them.\n let clearedBefore = 0;\n // Other tabs using the same outbox, which loaded the same entries: tell them what's gone. Not\n // on a server, where an open channel would keep the process alive.\n const channel =\n typeof window !== 'undefined' && typeof BroadcastChannel === 'function'\n ? new BroadcastChannel(`react-websocket-outbox:${options.name}`)\n : undefined;\n let disposed = false;\n let release: (() => void) | undefined;\n // A strictly increasing `createdAt`, so entries added in the same ms keep their order.\n let lastCreated = 0;\n\n function changed() {\n snapshot = [...entries.values()];\n // Hold the connection while anything is waiting, so it gets delivered.\n if (snapshot.length && !release && !disposed) release = client.acquire();\n else if (!snapshot.length && release) {\n release();\n release = undefined;\n }\n for (const listener of [...listeners]) listener();\n }\n\n function drop(id: string) {\n if (!loaded) removed.add(id);\n if (entries.delete(id)) changed();\n }\n\n function dropBefore(before: number) {\n clearedBefore = Math.max(clearedBefore, before);\n let changes = false;\n for (const entry of entries.values()) {\n if (entry.createdAt < before) changes = entries.delete(entry.id);\n }\n if (changes) changed();\n }\n\n // Also deletes a stored entry that isn't loaded yet, so it doesn't come back.\n function forget(id: string) {\n drop(id);\n channel?.postMessage({ id });\n return storage.delete(id);\n }\n\n function flush() {\n // The socket, not the status, which reads `open` while the server closes it. Not `volatile`\n // either, which can be a datagram that never arrives.\n if (!loaded || disposed || client.socket?.readyState !== 1) return;\n const now = Date.now();\n let sent = false;\n for (const entry of [...entries.values()]) {\n if (entry.sent) continue;\n if (now - entry.createdAt > maxAge) {\n forget(entry.id).catch(noop);\n options.onExpire?.(entry);\n } else if (client.send(options.send(entry.message, entry.id))) {\n if (!options.ack) forget(entry.id).catch(noop);\n else {\n entries.set(entry.id, { ...entry, sent: true });\n sent = true;\n }\n }\n }\n if (sent) changed();\n }\n\n const ready = storage\n .load()\n .then(records => {\n if (disposed) return;\n // Entries added meanwhile are newer than the stored ones, unless one has the same id.\n entries = new Map(\n [\n ...records.filter(\n record =>\n record.createdAt >= clearedBefore &&\n !entries.has(record.id) &&\n !removed.has(record.id)\n ),\n ...entries.values()\n ]\n .sort((a, b) => a.createdAt - b.createdAt)\n .map(record => [record.id, { ...record, sent: false } as OutboxEntry<M>])\n );\n changed();\n })\n // Even without its stored entries, the outbox works from memory.\n .finally(() => {\n loaded = true;\n removed.clear();\n flush();\n });\n // Nobody may be awaiting `ready`; don't surface that as an unhandled rejection.\n ready.catch(noop);\n\n /** What was sent on the last connection may not have arrived: send it again on the next. */\n function unsend() {\n let changes = false;\n for (const entry of entries.values()) {\n if (entry.sent) {\n entries.set(entry.id, { ...entry, sent: false });\n changes = true;\n }\n }\n if (changes) changed();\n }\n\n if (channel) {\n channel.onmessage = ({ data }: MessageEvent<{ id?: string; before?: number } | null>) => {\n if (data?.before) dropBefore(data.before);\n else if (data?.id) drop(data.id);\n };\n }\n\n const offs = [\n () => channel?.close(),\n // Also after `migrate()`, where the status stays `open`.\n client.on('open', () => {\n unsend();\n flush();\n }),\n client.subscribe(() => {\n if (client.getSnapshot().status !== 'open') unsend();\n }),\n client.on('message', message => {\n const id = options.ack?.(message);\n if (id != null) forget(String(id)).catch(noop);\n })\n ];\n\n return {\n client,\n ready,\n add(message, id = randomId()) {\n lastCreated = Math.max(Date.now(), lastCreated + 1);\n const record: OutboxRecord<M> = { id, message, createdAt: lastCreated };\n // Re-adding an id moves it to the end, as a new message.\n entries.delete(id);\n entries.set(id, { ...record, sent: false });\n changed();\n flush();\n // Sent, with no ack to wait for: there's nothing to keep. Storing it now would outlive its\n // delete, and send it again after a reload.\n if (!entries.has(id)) return Promise.resolve(id);\n return storage.put(record).then(() => id);\n },\n remove: forget,\n clear() {\n // At once, like `remove()`: waiting for the stored entries would send them first.\n const before = (lastCreated = Math.max(Date.now(), lastCreated + 1));\n dropBefore(before);\n channel?.postMessage({ before });\n // Including what other tabs stored, but not what's added from now on.\n return storage\n .load()\n .then(records =>\n Promise.all(\n records\n .filter(record => record.createdAt < before)\n .map(record => storage.delete(record.id))\n )\n )\n .then(noop);\n },\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n dispose() {\n disposed = true;\n for (const off of offs) off();\n release?.();\n release = undefined;\n }\n };\n}\n"],"mappings":"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"}