Undelivered Sync
When messages fail to deliver (e.g. the connection drops mid-send), you can
persist them to storage and retry later. The
createUndeliveredSync utility handles
this with a channel-based store backed by any async storage.
When You Need It
Without undelivered sync, messages that fail to send are simply lost. This is fine for ephemeral data like cursor positions, but for important messages (chat, commands, form submissions) you'll want persistence. There are two scenarios where messages need saving:
Connection drops mid-send:
- User sends a message while online — it enters the in-flight map
- Connection drops before the server confirms —
useSocketInFlightDropfires - Save the dropped messages to the undelivered store
- On reconnect,
onReadyretries them
User sends while already disconnected:
send()returnsfalse— the message never enters the in-flight map- Save it to the undelivered store immediately
- On reconnect,
onReadypicks it up and retries
Setup
import {
createUndeliveredSync,
createLocalStorage,
} from "@luciodale/react-socket";
type TUndeliveredMsg = {
id: string; // required — used for deduplication
channel: string;
text: string;
};
const undelivered = createUndeliveredSync<TUndeliveredMsg>({
storage: createLocalStorage(),
storageKey: "ws_undelivered", // optional, defaults to "ws_undelivered_messages"
});
// Load any previously saved messages from storage
await undelivered.init();Storage Interface
createLocalStorage() wraps
window.localStorage with error handling.
You can provide any storage that implements
IStorage:
import type { IStorage } from "@luciodale/react-socket";
const customStorage: IStorage = {
getItem(key: string): Promise<string | null> { /* ... */ },
setItem(key: string, value: string): Promise<void> { /* ... */ },
removeItem(key: string): Promise<void> { /* ... */ },
};All methods are async, so you can use IndexedDB, a server-side API, or any other async store.
API
// Initialize — must be called before using the store
await undelivered.init();
// Check if initialized
undelivered.isInitialized(); // boolean
// Get messages for a channel
undelivered.getChannelMessages("general"); // TUndeliveredMsg[]
// Add a message (no-op if the ID already exists)
undelivered.addMessage("general", { id: "abc", channel: "general", text: "hello" });
// Remove a single message by ID
undelivered.removeMessage("general", "abc");
// Replace all messages for a channel
undelivered.setChannelMessages("general", [msg1, msg2]);
// Clear a single channel
undelivered.clearChannel("general");
// Clear everything and remove from storage
undelivered.clearAll();
// Subscribe to changes (returns unsubscribe function)
const unsub = undelivered.subscribe(() => {
console.log("Undelivered store changed");
});Integration with the Manager
Wire the undelivered store into the manager. There are two save paths:
the useSocketInFlightDrop hook for messages
dropped on disconnect, and a check on
send()'s return value for messages sent
while already offline:
// Assumption: the server echoes both ackId and channel in the confirm
// so the bridge has enough info to remove the right entry.
type TServerMsg =
| { type: "delivered"; ackId: string; channel: string }
| { type: "chat"; /* ... */ };
const manager = new WebSocketManager<TClientMsg, TServerMsg>({
// ...
// Wire ack detection once — library clears in-flight automatically
getAckId: (msg) => (msg.type === "delivered" ? msg.ackId : undefined),
// Retry undelivered messages after every (re)connect
onReady() {
for (const channel of ["general", "random"]) {
for (const msg of undelivered.getChannelMessages(channel)) {
manager.send({
data: { type: "message", id: msg.id, channel, text: msg.text },
ackId: msg.id,
});
}
}
},
});
// Mount once in your app shell. The bridge owns the React-side wiring.
function UndeliveredBridge() {
// Save messages that were in-flight when the connection dropped
useSocketInFlightDrop(manager, (messages) => {
for (const { id, data } of messages) {
if (data.type === "message") {
undelivered.addMessage(data.channel, {
id,
channel: data.channel,
text: data.text,
});
}
}
});
// Remove from storage once the server confirms
useSocketEvent(manager, "delivered", (msg) => {
undelivered.removeMessage(msg.channel, msg.ackId);
});
return null;
}
// When sending, check the return value — if false, save immediately
function sendMessage(channel: string, text: string) {
const id = crypto.randomUUID();
const sent = manager.send({
data: { type: "message", id, channel, text },
ackId: id,
});
if (!sent) {
undelivered.addMessage(channel, { id, channel, text });
}
}Reactivity
Use the subscribe method to react to changes.
With Zustand, you can expose undelivered messages as part of your store:
import { useSyncExternalStore } from "react";
function useUndeliveredMessages(channel: string) {
return useSyncExternalStore(
(cb) => undelivered.subscribe(cb),
() => undelivered.getChannelMessages(channel),
);
}Next Steps
- Optimistic Updates — the in-flight tracking that feeds into undelivered sync
- Reconnection — when onReady fires and how to retry
- API Reference — full method list