Details
Expansion
Categories
Developers
Having issues?
DeltaSync — Efficient Data Sync Library for WoW Addons
For addon developers. DeltaSync is a LibStub library that handles the hard parts of keeping guild-member data in sync — version comparison, delta compression, peer-to-peer catch-up, and message integrity — so you can focus on your addon's actual features instead of writing another sync engine from scratch.
It's the same sync stack that powers TOGBankClassic and TOGProfessionMaster, extracted into a reusable library you can embed in a few lines.
Why Use This
If your addon shares data across guild members — inventories, crafting recipes, raid rosters, settings — you've probably run into some combination of:
- Full re-sends every time anything changes, hammering the guild channel
- CRC errors under load when several addons compete for chat throttle slots
- New members joining and having no idea who to ask for a data catch-up
- Home-rolled hashing that disagrees on tables because Lua iteration order is unstable
- One slow peer freezing everyone else because there's no timeout on expected data
DeltaSync solves all of these. Drop it in, wire up a handful of callbacks, and you get battle-tested sync behavior that's been running in production on live Classic Era guilds.
What You Get
Bandwidth-efficient sync
Instead of re-sending everything whenever something changes, DeltaSync computes and transmits only the differences between states — including deletions: a field one player removes is removed on every other player too (v4.1.0), instead of quietly living on. In practice this cuts sync traffic by 90-99% for incremental updates. Full sync is always available as a fallback for new members or major changes. The three ways an array diff can go quietly wrong — records keyed by their table address, a key-field list that does not cover what the key function reads, two records sharing one key — are caught at compute time and warned about once, and strictKeys turns them into errors for development builds, so a diff that would never converge fails on your machine rather than hours later on someone else's.
Peer-to-peer catch-up — two protocols, your choice per addon
Hash P2P (the default): when a player logs in missing data, DeltaSync broadcasts a summary of what they have and lets any peer with fresher data step up. No single "banker" bottleneck, no manual request dance. If the first peer is busy, it politely declines and the next best peer takes over automatically.
Numbered P2P (v4.1.0, opt-in): for addons whose records carry a version identity — a publish time plus a content hash. Every record gets a four-digit number agreed across the whole guild, so an offer is a handful of digits rather than a list of names and hashes. A peer offers only when it holds a strictly newer version; before anyone is asked for data, the offerers are asked which version they hold; the fetch goes to the holders of the newest version only, and if they are all busy the requester waits its turn in their queue rather than settling for an older copy. A busy provider queues requests instead of refusing them, and a request that names a version the provider has since moved past is served with the newer one. The record's author is treated as the authority: when the author has already announced what it holds, the version query is skipped for that record; when it has to run, the author is asked first and its answer settles it (v4.2.2 made this work for authors on your own realm — see Recent Updates). This is the protocol TOGBankClassic runs on a live guild, generalised so any addon can use it.
As of v4.2.0 it also covers the cases a real guild hits that a clean test run does not. A player whose broadcast leaves a record out (a wiped install, a new character) is offered that record by anyone who has it, and if that player's number table is out of date, the table is sent first so the offer makes sense. An offer that arrives before the number table it depends on, which happens routinely under chat throttling because the one-chunk offer overtakes the multi-chunk table, is held and used the moment the table lands instead of being thrown away and costing a two-minute wait for the next catch-up.
As of v4.4.0 a record's number is only ever read through the table that issued it. Two players whose number tables differ could otherwise read the same number as two different records, credit one record's version to another, and ask each other for versions nobody held. A message numbered on a newer table now waits until that table arrives. A player on an older table that yours did not grow from is sent the current one and offered everything, since what they listed cannot be read. A guild's very first sync, when nobody holds a table yet, is held and replayed once the table lands rather than wasted, and a fetch that fails clears the "update offered" state instead of leaving it up until a reload.
Message integrity you can trust
Every structured message is wrapped with a checksum and a stop-marker. On the receive side, truncation and corruption are detected separately — so if a message got mangled mid-flight, you find out instead of silently applying garbage. Corrupt messages are logged with diagnostic info, not quietly dropped.
You are told whether each message actually went out
WoW silently discards addon messages under congestion, and most libraries never notice. DeltaSync checks every send and records the verdict; since v4.1.0 it can also tell your addon, per message, the moment a send is delivered, refused, or never attempted — exactly once, including sends the library itself declined (an offline target, a misconfigured channel). An addon that spreads load across guild members can wait until a reply has genuinely left the client before taking on the next request, instead of guessing from when it was queued.
Modest prefix usage, and a loud failure if the client refuses one
DeltaSync spends 7 addon-message prefixes per consuming addon, one per channel type, auto-derived from your addon's name so you never pick unique strings yourself.
AceComm discards the return value of RegisterAddonMessagePrefix — so if the client ever refuses a registration, messages simply never arrive on that prefix, with no error and no warning. DeltaSync reads the client's result code itself (v4.2.2 — earlier releases tested for a true/false the client never returns, so the warning could not fire): a full prefix list or an invalid prefix string names the dead channel in the debug log, in chat, and in host.prefixRegistrationFailed, while the routine "already registered" answer AceComm's own registration produces on every login is left silent.
(Earlier versions of this page said "WoW caps each addon at 16 communication prefixes". That was wrong: the 16 is AceComm's prefix-string length limit, not a count. The real cap on the number of prefixes is not something we have verified, so we no longer quote a figure. Caught by the TOGBankClassic library audit.)
Multiple addons in one client, with zero cross-talk
New in v4.0.0: DeltaSync:NewHost(config) returns an isolated per-host object that owns its own namespace, prefixes, callbacks, peer state, and P2P sessions. Two — or ten — addons in the same client each call NewHost and run completely independent sync, with no shared state to clobber. (Before v4.0.0 the library was a singleton: whichever addon initialized last silently took over the others' prefixes and callbacks.) You call every DeltaSync method on the handle you hold, so the addons never see each other's traffic. The old DeltaSync:Initialize(config) call still works as backward-compatible sugar for a single consumer.
Guild roster tracking via LibGuildRoster-1.0
DeltaSync uses LibGuildRoster-1.0 — the standalone LibGuildRoster addon — to track who's in your guild and who's online, so it can avoid whispering offline members and default its peer-eligibility check. Since v4.4.0 that covers allied (sister) guild members too: a whisper to one LibGuildRoster does not see online is not sent, and any allied member who messages you is recorded as online, so a reply to them goes out (with a current LibGuildRoster). It also sets the one spelling DeltaSync uses for a player: every sender name your callbacks receive and every peer name in DeltaSync's own tables is the full Name-Realm that LibGuildRoster keys its roster by (v4.3.0+), normalised the moment a message arrives — so your addon compares names directly against the roster and never strips or appends a realm itself. On World of Warcraft: Forever, where characters have no realm and the server refuses a whisper addressed with one, DeltaSync addresses its whispers by the plain full name (v4.4.1+) while your addon still sees Name-Realm. It's a required dependency that CurseForge installs automatically alongside DeltaSync; DeltaSync also feature-detects each method, so it degrades to reasonable inline defaults if the library is somehow unavailable. (As of v3.0.0 this replaces the previously-bundled GuildCache-1.0, which has been retired — see Recent Updates.)
Cross-guild roster sharing (optional)
New in v3.1.0: RosterSync lets confederated guilds share their member lists. Your addon's /who discovery finds an online member of an allied guild and calls host:RequestRosterSync(peerName) — DeltaSync whispers that peer, compares membership hashes, pulls the roster only if it changed, and feeds it into LibGuildRoster's sister-roster store, all over the existing whisper channels (no extra prefix, no guild broadcast). Entirely opt-in via host:InitRosterSync(config); addons that never call it are completely unaffected.
Guild-mode for whisper-broken servers (optional)
New in v3.2.0: some private/emulated cores — notably Whitemane — don't deliver addon messages over whisper, which silently breaks the directed query/response/delta channels. Guild-mode is a user-toggled fallback that reroutes those directed channels onto the guild channel, stamping each message with its intended recipient so every other member drops it on receipt — directed behavior over a broadcast transport, no whisper required. Opt-in via host:InitGuildMode(config) / host:SetGuildMode(enabled); the host owns the settings toggle, and addons that never enable it are byte-identical to before.
Custom chat-channel transport (optional)
For addons that must talk over a named chat channel rather than the guild — realm-wide or cross-guild reach — any of the seven channels can be given distribution = "CHANNEL". Switch the transport on with channelModule = { enabled = true, serializer = ..., deserializer = ... } in your NewHost config; without it a CHANNEL prefix can send but nothing listens for replies. DeltaSync then sends over SendChatMessage and receives over CHAT_MSG_CHANNEL through the same handlers. The channel must be named: since v4.1.0 a CHANNEL prefix with no name is refused rather than silently listening on every channel the player is in, and a wildcard has to be spelled channel = "*" so it is a decision somebody made. Messages on this transport are capped at 255 bytes by the client, so hosts supply a compact serializer; the checksum envelope still catches a truncated one.
Content hashes that agree across clients
Sync decisions rest on hashes, so DeltaSync's are deterministic over Lua tables regardless of iteration order, and two logically equal structures hash equal whether or not they happen to share an inner table (v4.1.0). Revision 1 is frozen — its output is compared between clients and can never change — and revision 2 additionally tells 1 from "1". host:MakeHashEntry carries both, and the P2P layer compares on the highest revision both ends advertise, so a mixed-build guild upgrades one player at a time. The README also spells out when to hash — once, at save, by the author of the change, then forwarded verbatim — because getting that wrong produces a sync that reports itself healthy while clients run different data.
Diagnostics you can route into your own addon
host:DebugStatus() prints identity, prefixes, channel configuration, whether P2P is loaded, roster readiness and the delivery counters in one block — paste it into any bug report. Debug output is organised by category and tag, each toggleable, with an optional chat tab. If your addon already has a debug system, don't run two: register your categories into DeltaSync's with host:RegisterDebugCategory, or hand it your logger with config.logger and it stops filtering, buffering and claiming a tab. config.onDebugMessage is a tap that fires alongside DeltaSync's own output with the structured category and tag, so the library's sync diagnostics land in your persistent log and export.
Version reporting
The installed DeltaSync addon registers with VersionCheck-1.0 (v4.2.0+), so guild members can see who runs which DeltaSync in the /vc window and players who are behind are prompted to update. A copy you bundle inside your own addon never registers on DeltaSync's behalf.
Tested offline, against the real libraries
DeltaSync ships an offline test suite (722 specs as of v4.4.1) built on the shared WoWAPITesting harness. Every player in a test is a separate simulated client that loads its own copy of DeltaSync, AceComm, AceCommQueue, AceSerializer-3.0, LibGuildRoster-1.0 and VersionCheck-1.0 — the real libraries, not stubs — and sends through the real chat-throttling code onto a shared wire that routes guild traffic to guildmates and whispers to the named player, so complete broadcast → offer → handshake → deliver → apply cycles between two or three players are asserted end to end exactly as the client would run them (v4.2.2; earlier releases used a hand-written stand-in network, and both v4.2.2 fixes are what the real wire turned up). Every reachable line is covered; the handful that cannot execute are listed individually in the README with the reason. Since v4.1.0, every fix in the Recent Updates below has a spec that fails without it.
Dependencies
- Ace3 (required) — provides LibStub, AceComm, AceSerializer
- AceCommQueue-1.0 (required) — serializes outgoing messages so chunked payloads don't interleave and corrupt each other. Your host addon embeds this alongside Ace3.
- LibGuildRoster (required) — the guild-roster engine described above. CurseForge installs it automatically when you install DeltaSync.
- VersionCheck-1.0 (required, v4.2.0+) — lets guildmates see which DeltaSync version each player runs, in its
/vcwindow, and prompts players who are behind to update. CurseForge installs it automatically. Only the installed DeltaSync addon registers; a copy you bundle inside your own addon never reports a version on DeltaSync's behalf.
All four are declared as hard Dependencies: in the TOC, so WoW loads them before DeltaSync.
Supported Game Versions
One package, one TOC, every flavour: Classic Era, World of Warcraft: Forever (v4.2.1+), The Burning Crusade Classic, Wrath of the Lich King Classic, Cataclysm Classic, Mists of Pandaria Classic, and Retail. The Wrath entry also covers the Whitemane private server's build, which is why Guild-mode exists. DeltaSync is developed and tested on Classic Era; the other flavours load and are believed to work, but are not exercised by the author. On Forever, whispers are addressed without a realm (v4.4.1), as that server requires; saved data not persisting between sessions there is known and not yet handled.
Quick Start
Basic setup
-- In your addon's OnInitialize / OnEnable:
local AceAddon = LibStub("AceAddon-3.0")
local AceCommQueue = LibStub("AceCommQueue-1.0")
local DeltaSync = LibStub("DeltaSync-1.0")
-- 1. Make sure your AceAddon instance has AceComm and AceCommQueue
local MyAddon = AceAddon:NewAddon("MyAddon", "AceComm-3.0")
AceCommQueue:Embed(MyAddon)
-- 2. Create your isolated DeltaSync host (v4.0.0+). HOLD the returned handle —
-- call every DeltaSync method on it, never on the shared LibStub handle.
local host = DeltaSync:NewHost({
namespace = "MyAddon",
aceAddon = MyAddon, -- REQUIRED — DeltaSync sends through your addon
onDataReceived = function(sender, data, len)
-- apply received data or delta
end,
onDataRequest = function(sender, baseline)
-- peer asked for data; call host:SendData(sender, myData)
end,
})
Sending and receiving
-- Announce your current state to the guild host:BroadcastVersion(myVersion, myHash) -- Respond to a data request host:SendData(sender, myData, false) -- full sync host:SendData(sender, myDelta, true) -- delta sync
Peer-to-peer catch-up
host:InitP2P({
-- MakeHashEntry carries both hash revisions, so mixed-build guilds agree.
getMyHashes = function()
local out = {}
for key, value in pairs(myData) do
out[key] = host:MakeHashEntry(value, value.updatedAt)
end
return out
end,
hasContent = function(key) return myData[key] ~= nil end,
hasMissingItems = function() return nextMissingItem ~= nil end,
onSyncAccepted = function(key, sender)
host:RequestData(sender, myBaseline)
end,
})
host:BroadcastItemHashes(myItemHashes)
Numbered peer-to-peer catch-up (v4.1.0, opt-in)
-- For records that carry a version identity (a "canon": publish time + content hash).
host:InitNumbers({
table = function() return MyAddon.db.numbers end, -- your SavedVariables; three fields are kept here
keys = function() return MyAddon:RecordKeys() end, -- every key eligible for a number
canMint = function() return MyAddon:OwnsARecord() end,
})
host:InitP2P({
mode = "numbered",
servableCanon = function(key) return MyAddon:CanonICanServe(key) end,
onDeliver = function(key, canon, provider)
host:RequestData(provider, { type = "leaf-data", parent = key, hash = MyAddon:HeldCanon(key) })
end,
-- optional: peerCapable(name), isValidPeer(name), onAdvertised(key, canon, peer),
-- onNewerOffered(key, peer), onNewerCleared(key), onSelfConsulted(reason)
-- optional: hasMissingItems() -> bool -- default false, and while false the
-- library never re-broadcasts to catch up on its own
hasMissingItems = function() return MyAddon:IsMissingAnything() end,
-- optional (v4.2.0+): the same extra fields on the library's own catch-up broadcasts
broadcastExtra = function() return { addon = MyAddon.version } end,
})
host:BroadcastNumbered("BULK", { addon = MyAddon.version })
-- onOfferReceived (NewHost config) fires BEFORE the protocol judges a broadcast (v4.2.0+),
-- so read what it says about its sender there -- peerCapable sees the result.
-- In onDataRequest: host.p2p:QueryArrived(sender, key) claims the accepted slot,
-- then host.p2p:ServeReply(sender, key, data, isDelta) releases it when the reply has drained.
Who Should Use This
- Addons that sync structured data (inventories, rosters, recipes, settings) across guild members
- Addons where incremental updates are common and full re-syncs are wasteful
- Addons that want new members to catch up from any peer, not just a single broadcaster
- Addons that want CRC-level message integrity without writing it themselves
What You Don't Have to Write
- AceComm prefix registration and routing
- A wire format that survives partial delivery and chat-throttle chunk interleaving
- A P2P collect/offer/dispatch pipeline with timeouts and busy fallback — or the numbered one, with version queries, per-peer queues and newest-holder-only dispatch
- A guild-wide numbering table that every client agrees on without a central authority
- Out-of-order delivery under chat throttling: an offer that overtakes the number table it depends on is held until the table lands, not lost
- Version reporting for the library itself: DeltaSync appears in VersionCheck's
/vcwindow on its own - Delivery tracking: knowing which of your messages the client actually accepted
- Stable content hashing for Lua tables (iteration order is not your friend, and neither is a shared sub-table)
- Deletion handling in a delta format (Lua has no way to say "this field is gone" — DeltaSync does)
- Consistent player names: every sender your addon is handed is already the full
Name-Realm, whether the player is on your realm or a connected one - Not whispering people who have logged off, in your guild or an allied one
- A categorised debug system with a chat tab, or the plumbing to route the library's diagnostics into the one you already have
Recent Updates
v4.4.1 (Current) — Whispers reach players on World of Warcraft: Forever. Library revision MINOR 23. Nothing changes on any other game version, nothing changes in what your addon's callbacks receive, and no addon needs to change code.
Fixed
- On World of Warcraft: Forever, syncs with a guildmate failed with "No player named '... -Realm' is currently playing". Forever characters have no realm, and the server refuses a whisper addressed to a name with one. DeltaSync now addresses its whispers there by the plain full name. Every other game version is addressed exactly as before. Not yet tried on a Forever client.
v4.4.0 — Numbered sync never mixes up two records, allied guilds are guarded, and DeltaSync is filed under Library. Library revision MINOR 22. A guild running v4.3.0 and v4.4.0 side by side keeps syncing: the one new wire field is optional, and a player without it is read the old way. One cost while builds are mixed: a v4.3.0 player asking a v4.4.0 player on a different number table can be told "nothing newer" and drop an update that player could have served, until both are on v4.4.0. No addon has to change code; addons using numbered sync get every fix below for free.
New
- DeltaSync appears under "Library" in the in-game addon list, alongside the other libraries and in the same orange, instead of among your gameplay addons.
- A guild's first numbered sync no longer stalls. When nobody holds the number table yet, the first announcement is held until the table arrives and then acted on, including offering back what the listener has. Before, the listener learned nothing from it until somebody announced again. Addons can detect this with
host.p2p.ParkBroadcast.
Changed
- Whispers to allied (sister) guild members are skipped while they are offline, as whispers to your own guild already were, so a sync with someone who has logged off stops sending into "No player named ...". An allied member who messages you is counted as online, so replies to them still go out (with a current LibGuildRoster).
- A player whose number table is one yours did not grow from is now offered every record the other player can serve, since what they announced cannot be read against a different table.
- Because of that, an addon that shows "update offered" may briefly show it on every record for such a player, until each record's version has been checked.
Fixed
- Numbered sync could credit one record's version to a different record when two players' number tables differed, so players asked each other for versions nobody held and the sync never settled. A number is now only read through the table that issued it.
- "Update offered" could stay up until a reload after a fetch that nobody could deliver, or after the player who offered it switched to a newer number table. Both now clear it.
- Retail showed DeltaSync, and every addon that requires it, as out of date. The current retail game builds are now declared.
v4.3.0 — Every player name your addon receives now carries its realm. Library revision MINOR 21. Nothing changes on the wire, and a guild running v4.2.x and v4.3.0 side by side keeps syncing. This is a change in what your addon's callbacks are handed, so read the one item below before adopting.
Changed
- The sender name passed to every callback is now the full
Name-Realm, whether the player is on your realm or another one. The game client spells a same-realm player without the realm, and until now DeltaSync passed that spelling straight through to youronDataReceived,onDataRequest,onVersionReceivedand numbered-sync callbacks, while its own peer tables used the full name. An addon comparing a sender against its guild roster had to know which spelling it was holding. Now it never does: DeltaSync normalises the name the moment a message arrives, so every callback and every peer table agrees with LibGuildRoster. If your addon strips or appends a realm itself, or compares a sender against a bare name, delete that — an addon that already normalised through LibGuildRoster is unaffected. Detect it withLibStub("DeltaSync-1.0").MINOR >= 21.
v4.2.2 — Two fixes that only showed up on a real wire. Library revision MINOR 20. Nothing changes on the wire and no addon needs to change anything: both fixes are inside the library. The test suite now runs every player as a separate client through the real chat libraries instead of a hand-written stand-in, which is what exposed them.
Fixed
- A prefix the game client refuses to register is now actually reported. That warning has been advertised since v4.0.2, but the check compared the wrong kind of answer (the client replies with a result code, not true/false), so it could never fire in game. It now reads the client's result: a full prefix list or an invalid prefix is named in chat and the debug log, and the everyday "already registered" answer, which every login produces, is correctly left silent.
- Numbered sync never recognised the author of a record on the same realm. The client delivers a same-realm sender's name without the realm, and three places compared it against the realm-qualified key, so the shortcut that skips the version query when the author has already announced never applied to anyone on your realm. Every sync paid one extra round of messages it did not need. Names are now normalised before comparing.
v4.2.1 — Loads on World of Warcraft: Forever. A one-line release: no library code changed and the library revision stays at MINOR 19, so nothing changes on the wire and no addon needs to do anything.
New
- DeltaSync now declares support for World of Warcraft: Forever (the Classic Era beta on the modern client), so it loads there instead of being refused as out of date. Any addon that depends on DeltaSync needed this before it could load on Forever itself. Not yet tried on a Forever client; two things Forever does differently, characters without realm names and saved data that does not persist between sessions, are known and not yet handled by the library.
v4.2.0 — DeltaSync shows up in VersionCheck, and the numbered sync protocol copes with a real, congested guild. Library revision MINOR 19. Nothing changes on the wire: no new message type and no new prefix, so a guild running v4.1.0 and v4.2.0 side by side keeps syncing. Addons using the numbered protocol get every change below without touching their code.
New
- DeltaSync reports its version through VersionCheck-1.0. It gets its own tab in the
/vcwindow, and players on an older DeltaSync are prompted to update. VersionCheck is now a required dependency that CurseForge installs for you. - Numbered sync offers what a player has none of. When a player announces what they hold and leaves a record out entirely (a wiped install, a new character), anyone who has it now offers it. Before, only records the player already had an older copy of were ever offered.
broadcastExtra, a new optional numbered-sync setting, so the library's own catch-up announcements carry the same extra fields (your addon version, say) as the ones your addon sends.
Changed
- A player behind on the guild's number table is sent the table before any offer that depends on it.
- Your
onOfferReceivedcallback now runs before the sync acts on the message, as the documentation always said. An error inside it is reported and no longer stops the sync. - To check that a player's DeltaSync is new enough for the changes above, test for
host.p2p.OnNumbersChangedon your numbered instance, orLibStub("DeltaSync-1.0").MINOR >= 19. The method name is kept stable for exactly this.
Fixed
- An offer that arrived before the number table it depends on was thrown away. Under normal chat throttling that is the usual order, so a player catching up waited about two minutes for the next catch-up round. The offer is now held and used as soon as the table arrives.
- With the old callback order, the very first announcement from a player on an older release could start a sync that release could not finish, which then sat until it timed out.
v4.1.0 — A second, numbered sync protocol; deleted data now stays deleted everywhere; and the peer-review fixes. Library revision MINOR 18; every change is additive, so a guild running mixed versions keeps working and can upgrade one player at a time. Addons that change nothing get the fixes for free; the new protocol and the number table are opt-in and inert until an addon asks for them.
New
- Numbered P2P — a second catch-up protocol an addon can choose instead of the default. Records are named on the wire by a four-digit number every client agrees on, so an offer is a few digits rather than a list of names and hashes. A peer offers only when it holds a strictly newer version; offerers are asked which version they hold before anyone is asked for data; the fetch goes to the newest holders only and waits in their queue rather than settling for an older copy; a busy provider queues requests instead of refusing them. It is the protocol TOGBankClassic has been running on a live guild, now available to every addon.
- A guild-wide number table — the numbering the protocol above depends on: issued once, kept for life, never reused, and converging to one table across every client with no central authority.
- Addons can now be told when each message has actually finished sending — delivered, refused, or never attempted — rather than only when one was refused. An addon that spreads load across guild members can wait until a reply has genuinely left the client before taking on the next request, instead of guessing from when it was queued.
Changed
- An addon that listens on a custom chat channel must now name the channel. Before, forgetting the name made it listen on every channel the player was in — General, Trade, all of them.
- The written guidance on content hashes now matches how a real addon has to build them: the timestamp travels alongside the hash, and an addon that uses the hash to decide whether anything changed keeps the timestamp out of it.
- The Wrath interface value stays at
38000, which is the Whitemane build; a review that briefly moved it was reversed before release.
Fixed
- Deleting a field from a record never reached other players. The change was detected and then dropped before it was sent, so everyone else kept the old value forever while both sides reported themselves in sync. Deletions are now transmitted, at every level.
- Two records with identical content could compare as different when one happened to share an inner table and the other held two copies of it, making players offer each other data across a difference that did not exist.
- A sync request from someone outside the guild could tie up all of a player's outbound send capacity for ninety seconds; requests are now checked for eligibility before anything is committed.
- Sends could be counted as still in progress after they had finished when an addon named the requester differently from how the message arrived, stalling further sends until a safety timer cleared them.
- A queue of pending sync work could grow without limit during a long session with one slow provider.
v4.0.3 — A refused send is no longer invisible. DeltaSync never checked whether its messages actually went out. WoW silently discards addon messages under congestion, and AceComm-3.0 forwards only ChatThrottleLib's didSend boolean — so a refused message simply vanished, and a stalled sync gave no clue why. Every send now passes a delivery callback and records the verdict in host.sendsDelivered / host.sendFailures / host.lastSendFailure, logs it plainly as "the message did NOT arrive", and fires the new optional config.onSendFailed so your addon can re-send, degrade, or tell the user instead of assuming delivery. DebugStatus reports the counters, the last refusal, and whether your addon actually embedded AceCommQueue-1.0 — an unqueued host being exactly the one exposed to the chunk-interleaving corruption that library exists to prevent.
- Handles both callback shapes. AceCommQueue-1.0 delivers one terminal callback carrying the whole-message verdict; raw AceComm delivers one per chunk. A check written for one shape misbehaves under the other, so both are modelled in the test suite — including a 5-chunk send with only its 3rd chunk refused, which a naive check records as failed and delivered. A partial multipart stream reassembles into a corrupt payload, so the only correct reading is that it did not arrive.
- All the verdict states are handled, and the distinction that matters is not the obvious one. Three different situations share a
nilverdict: your own wrapper suppressed the send, the queue rejected a bad argument, or the send raised. The last two mean the message did not arrive and are counted as failures; only a deliberate suppression is not — counting that would give an addon with a raid-guard wrapper a forever-climbing error count for behaving correctly. So the rule is "was it suppressed?", not "was it refused?". Where AceCommQueue-1.0 v1.0.5+ supplies itsreasonargument DeltaSync uses it; against an older copy that cannot say, the send is recorded as not-attempted rather than guessed at. - Purely additive. No API removed, renamed or reordered; sending behaviour itself is unchanged. An addon that changes nothing gains the diagnostics for free. Library revision bumped to MINOR 17; feature-detect with
DS.MINOR >= 17. - Deprecated:
config.hashStrategyis inert and commented out, pending removal. It was stored and then read by nothing, so"deep"and"shallow"always behaved identically. Passing it remains harmless.
v4.0.2 — Timer cancellation actually works now, found by a new offline test suite. Four P2P timers were created with C_Timer.After and stored so they could be cancelled later — but only C_Timer.NewTimer returns a handle carrying :Cancel(), so every one of those cancels was a silent no-op sitting behind an if timer then guard. Most visibly, extending the offer collect window did not extend it: the original timer survived and dispatch fired at the old deadline anyway, discarding offers that arrived during the extension, then fired a second time later with the window already closed. On a busy login — where hash-list broadcasts arrive in bursts and repeatedly re-open the window — that cost real offers. All four timers now use NewTimer. No wire-format or API change; a MINOR 16 host and a MINOR 15 peer interoperate exactly as before.
- Offline test suite added — 509 specs at 99.54% line coverage across all six library files, running in milliseconds with no game client, built on the shared WoWAPITesting harness. It loads the real AceSerializer-3.0 and LibGuildRoster-1.0 rather than stubs, and a loopback AceComm network lets two or three DeltaSync hosts genuinely sync with each other so complete broadcast → offer → handshake → deliver → apply cycles are asserted end to end. The environment models the WoW API faithfully rather than conveniently — that is precisely why the timer bug above showed up instead of passing.
- Packaging — the
.pkgmetaignore list was rewritten to the packager's documented syntax (bare folder names, single-star repo-relative globs, no dotfile entries), andTestswas added so the new offline suite never ships to players. The published zip contains only the six library files, the TOC, README, CHANGELOG and LICENSE. - Interface versions refreshed to the current build per flavor —
11509, 20506, 30405, 38000, 40402, 50504, 120007. Entries superseded by a newer build of the same flavor were dropped; every flavor DeltaSync shipped for is still covered. - Content-hash revision 2 — the old hash rendered a number and its string form identically (
{v = 1}and{v = "1"}hash the same, as dotrueand"true"), so a value stored as text looked unchanged to a peer storing it as a number and the sync was skipped. The newComputeHashV2tags each scalar with its type. It ships alongside revision 1 rather than replacing it —ComputeHashis unchanged and now explicitly frozen, because its output is compared between clients: build hash-list entries with the newMakeHashEntryand each one carries both, with the P2P layer comparing on the highest revision both ends advertise. A peer on an older build sends only the revision-1 value, and that absence is the signal to fall back — so mixed-build guilds agree instead of offering each other data forever. Upgrade one player at a time; the fix switches itself on per pair. Retiring revision 1 later changes one library function and no consumer call sites. - Send slots are now capped correctly — a send that completed normally left its safety timer armed, and when that fired it released a slot belonging to a different, later send from the same peer. Because that is an over-release rather than an underflow, the existing guard never caught it and
maxActiveSendscould be quietly exceeded under sustained load. Each safety timer is now tied to the acquisition that scheduled it. - Silent keying bugs in the delta engine now report themselves. Three ways a diff could go quietly wrong, all of which produced a plausible-looking delta and only surfaced as behaviour hours later on someone else's client:
- Records with no
id/ID/key/nameand nokeyFuncwere keyed by their table address — unstable across sessions and between clients, so every diff reported everything added and everything removed and never converged. keyFieldssilently had to cover every fieldkeyFuncreads, or removals matched nothing on the receiving side and deleted items lingered forever.- Two records sharing a key emitted two entries for one target, with the survivor decided by array order — so two clients whose order differed kept different records and re-synced forever.
options.strictKeysturns them into errors for development builds. - Records with no
- Logging is now a choice, in both directions. If your addon has its own debug system, stop running two: either register your categories into DeltaSync's with
host:RegisterDebugCategory(name, tags)and delete your tab, buffer and registry — or passconfig.loggerand take logging over entirely, in which case DeltaSync stops filtering, stops buffering and never claims a chat tab. The first is recommended unless you already own an output layer with log levels and user-facing messages. - Keep your own persistent log and export, and get DeltaSync's diagnostics into it. DeltaSync's buffer is deliberately session-only, in-memory and capped — retention costs your SavedVariables, so it is your policy to set, not a library's. The new
config.onDebugMessage(message, category, tag)is a tap: it fires alongside DeltaSync's own tab rather than replacing it, hands you the structured category and tag as well as the text, skips anything your own filtering suppressed, and routes a throwing sink togeterrorhandler()instead of swallowing it. So the library's own sync diagnostics finally land in your bug reports. - A prefix the client refuses to register is reported instead of becoming a silently dead channel. AceComm discards that return value, so past the client's registered-prefix limit messages simply never arrive, with no error and no warning. DeltaSync checks it, names the dead channel, and records it for
DebugStatus. - A cyclic record no longer hangs the client — the deep-equality comparison recursed with no cycle detection.
- Library revision bumped to MINOR 16. Everything above is additive: no API was removed, renamed or reordered, and an addon that changes nothing keeps working exactly as it does today. See the adoption checklist in the README for what each new capability costs to pick up.
Thanks to the TOGBankClassic library audit, which independently found three of the defects above before its own migration onto DeltaSync — and caught a documentation error here that had been repeated for several releases (the "16 prefixes per addon" figure is AceComm's prefix-string length limit, not a count).
v4.0.1 — Classic Era interface bump: the TOC's Classic Era interface level moves 11508 → 11509 so the library is no longer flagged out-of-date on the current Classic Era client. TOC-only — no library code changed, the LibStub revision stays at MINOR 15, and every other supported flavor is untouched.
v4.0.0 — Multi-host: DeltaSync is no longer a singleton. DeltaSync:NewHost(config) returns an isolated per-host object that owns its own namespace, prefixes, callbacks, peer state, and P2P / RosterSync / guild-mode instances — so multiple consuming addons in one client coexist without clobbering each other's prefixes and callbacks. (Previously the library was a singleton and whichever addon called Initialize last silently took over the rest.) The wire format is unchanged, so a v4.0.0 host interoperates with any existing peer.
DeltaSync:NewHost(config)— the new entry point;configis identical in shape to the oldInitialize. Hold the returned handle and call every method on it:DS:Method(...)becomeshost:Method(...), andDS.p2p:OnItemCompleted(...)becomeshost.p2p:OnItemCompleted(...). Delta ops, serialization, InitP2P, RosterSync, guild-mode, andDebugStatusare all called on the host.- Backward compatible —
DeltaSync:Initialize(config)still works, now as sugar over an implicit "default host", so a single un-migrated consumer is unaffected. Two consumers both onInitializestill share the one default-host slot and clobber each other, so migrate all but at most one consumer toNewHost. - Library revision bumped to MINOR 15, and
lib.MINORis now actually set on the handle (previously documented for feature-detection but never assigned). Detect the multi-host API withDeltaSync.NewHost and LibStub("DeltaSync-1.0").MINOR >= 15.
v3.2.1 — Send-size telemetry: lib:BroadcastData and lib:BroadcastItemHashes now return the serialized payload size (bytes) as an additive second value alongside the existing ok boolean — the same #message metric the receive path already reports — so a host can log accurate outbound send sizes without re-serializing. Purely additive and backward-compatible: existing single-return callers (local ok = lib:BroadcastData(...)) are unaffected, and the early failure paths still return a single value.
- Library revision bumped to MINOR 14 — feature-detect with
LibStub("DeltaSync-1.0").MINOR >= 14before reading the new second return value.
v3.2.0 — Guild-mode: an opt-in, user-toggled mode for private/emulated servers (e.g. Whitemane) that don't deliver addon whispers. It reroutes the five directed channels (QUERY, RESPONSE, DELTA, the directed OFFER reply, HANDSHAKE) from whisper to guild, stamping each directed send with its intended recipient so every other guild member drops it on receipt — identical end behavior to a whisper, with no new prefix and no wire-format change for anyone with it off. Purely additive: a consumer that never calls lib:InitGuildMode is byte-identical to v3.1.0.
lib:InitGuildMode(config)/lib:SetGuildMode(enabled)/lib:IsGuildMode()— opt-in init (applies a host-persisted toggle, firesonChangedon every flip) plus the runtime toggle and state query. The host owns the settings checkbox and its persistence; the library ships no UI, SavedVariables, or slash commands.- Recipient-stamped directed broadcasts — each directed GUILD send is prefixed with an out-of-band