Details

Addon ID: 24729
Addon Version: 4.4.1
Expansion: 1.60
Upload date: Oct 06, 2026
Last Updated: Oct 06, 2026
Downloads: Less than 100
Website: Source Link

Expansion


Categories


Developers

Pmptasty
Pmptasty Creator

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 /vc window, 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 /vc window 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 your onDataReceived, onDataRequest, onVersionReceived and 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 with LibStub("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 /vc window, 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 onOfferReceived callback 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.OnNumbersChanged on your numbered instance, or LibStub("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 nil verdict: 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 its reason argument 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.hashStrategy is 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 .pkgmeta ignore list was rewritten to the packager's documented syntax (bare folder names, single-star repo-relative globs, no dotfile entries), and Tests was 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 do true and "true"), so a value stored as text looked unchanged to a peer storing it as a number and the sync was skipped. The new ComputeHashV2 tags each scalar with its type. It ships alongside revision 1 rather than replacing it — ComputeHash is unchanged and now explicitly frozen, because its output is compared between clients: build hash-list entries with the new MakeHashEntry and 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 maxActiveSends could 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/name and no keyFunc were keyed by their table address — unstable across sessions and between clients, so every diff reported everything added and everything removed and never converged.
    • keyFields silently had to cover every field keyFunc reads, 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.
    All three now warn once, and the new options.strictKeys turns them into errors for development builds.
  • 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 pass config.logger and 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 to geterrorhandler() 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; config is identical in shape to the old Initialize. Hold the returned handle and call every method on it: DS:Method(...) becomes host:Method(...), and DS.p2p:OnItemCompleted(...) becomes host.p2p:OnItemCompleted(...). Delta ops, serialization, InitP2P, RosterSync, guild-mode, and DebugStatus are 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 on Initialize still share the one default-host slot and clobber each other, so migrate all but at most one consumer to NewHost.
  • Library revision bumped to MINOR 15, and lib.MINOR is now actually set on the handle (previously documented for feature-detection but never assigned). Detect the multi-host API with DeltaSync.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 >= 14 before 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, fires onChanged on 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 29<recipient>29 header (outside the CRC envelope); receivers drop messages addressed to someone else and strip their own before deserializing. 29 never begins an AceSerializer payload, so detection is unambiguous, and the stamp survives AceComm chunking. The recipient is realm-qualified at send time so same-named characters on different connected realms don't both match.
  • Interactions — RosterSync (cross-guild, whisper-only) self-disables while guild-mode is active, and the offline-member send guard applies to directed guild sends too, keeping undeliverable traffic off the shared channel.
  • Library revision bumped to MINOR 13 — feature-detect with LibStub("DeltaSync-1.0").MINOR >= 13 or if lib.InitGuildMode then.

v3.1.0 — RosterSync: optional cross-guild "sister roster" sharing for confederated guilds, layered on the existing WHISPER channels — no new prefix, no broadcast. A single call, lib:RequestRosterSync(peerName), whispers an online allied-guild member, compares membership hashes (pulling only when they differ), and feeds the result into LibGuildRoster's sister-roster store. Purely additive: a consumer that never calls lib:InitRosterSync is byte-identical to v3.0.0.

  • lib:InitRosterSync(config) / lib:RequestRosterSync(peerName) — opt-in init plus the one call a consumer makes. DeltaSync owns the wire, LibGuildRoster owns the store, and the host owns presence (its /who poll) and persistence. Membership-only, single round trip, provider-authoritative guild key.
  • lib:RegisterLeafType(prefix, handlers) — generic, additive leaf-type router: an optional module claims a leaf type prefix so its directed QUERY/RESPONSE traffic routes to it before the host's data callbacks. Roster traffic coexists with a host's own leaves (e.g. cooldowns/recipes) without either seeing the other's messages; a no-op when nothing registers.
  • Cooperative trust + isolation — a provider serves only its own guild's roster, the receiver rejects a roster whose stamped provider isn't in it, and roster traffic is directed-whisper-only (it never enters the guild-wide P2P offer path).
  • Library revision bumped to MINOR 12 — feature-detect with LibStub("DeltaSync-1.0").MINOR >= 12 or if lib.InitRosterSync then.

v3.0.0 — The bundled GuildCache-1.0 roster library has been retired in favor of LibGuildRoster-1.0 (the standalone LibGuildRoster addon), now a required dependency that CurseForge installs automatically. This is a breaking change for any addon that resolved LibStub("GuildCache-1.0") from DeltaSync's bundle. No wire-format change; the P2P sync protocol is untouched.

  • GuildCache-1.0 removed — DeltaSync no longer registers the GuildCache-1.0 LibStub MAJOR. Roster tracking, name normalization, and online-state come from LibStub("LibGuildRoster-1.0") instead.
  • Consumer migration — Addons that used DeltaSync's GuildCache should switch to LibStub("LibGuildRoster-1.0") and rename IsPlayerOnline → IsOnline and GetOnlineGuildMembers → GetOnlineMembers. NormalizeName, GetNormalizedPlayer, GetMember, and the roster callbacks keep their names. Note that IsInGuild is now a strict membership check rather than accept-all on an empty roster.
  • Graceful degradation preserved — DeltaSync feature-detects each library method, so it still falls back to inline defaults (realm derivation, skipped whisper guard, accept-all peers) when the library is absent or older.
  • Library revision bumped to MINOR 11 — Consumers can assert the new layout with LibStub("DeltaSync-1.0").MINOR >= 11.

v2.0.3 — Hash-mismatch offer condition for content-aware-merge consumers. The P2P offer path used to gate on updatedAt > peer.updatedAt, which suppressed legitimate offers from the actual data owner whenever a relayer's updatedAt happened to be higher (e.g. bumped on every RebuildAll even when content didn't change). v2.0.3 flips the condition to "offer whenever our hash differs from the peer's." The wire format is bit-identical to MINOR 8, so older consumers continue to interoperate.

  • Hash-mismatch offer condition — P2PSession:OnHashListReceived now offers data whenever its hash differs from the peer's, regardless of updatedAt. Required by content-aware-merge consumers (TOGProfessionMaster v0.2.0+) that resolve concurrent edits with merge-on-receive instead of last-writer-wins.
  • Candidate sort downgraded to heuristic — The descending-updatedAt insertion sort in OnOffer is retained but is no longer load-bearing for correctness. With content-aware merge on the receive side, dispatching to any candidate converges to the same answer; the sort now serves only as a "try the most-recently-updated peer first" tiebreak when peer load is equal.
  • Library revision bumped to MINOR 9 — Consumers that need to assert the new offer semantics can check LibStub("DeltaSync-1.0").MINOR >= 9.
  • Compatibility — No API or wire-format changes. Existing consumers (TOGBankClassic, etc.) that don't use the P2P offer path are entirely unaffected. Consumers that do use P2P will simply emit and observe more hash-offers when content actually differs. Adopting the new semantics on the receive side requires implementing content-aware merge in onDataReceived (max-wins for monotonic fields, union for sets).

v2.0.2 — GuildCache-1.0 gains CallbackHandler-1.0 events and real-time online/offline tracking via CHAT_MSG_SYSTEM, plus a login-race retry that fixes empty-roster results in the brief window after PLAYER_LOGIN. (GuildCache MINOR 2.)

  • GuildCache callbacks — Six events fire via CallbackHandler-1.0: OnRosterReady, OnRosterUpdated, OnMemberOnline(name), OnMemberOffline(name), OnMemberJoined(name), OnMemberLeft(name). Payloads are canonical "Name-Realm" strings. Subscriptions persist across LibStub upgrades.
  • Real-time CHAT_MSG_SYSTEM parser — Online/offline/join/leave transitions fire immediately on the system message instead of waiting for the next GUILD_ROSTER_UPDATE. The roster-rebuild diff still fires the same callbacks as a catch-up safety net.
  • Login-race retry — When IsInGuild() is true but GetNumGuildMembers() momentarily returns 0 after PLAYER_LOGIN, GuildCache now re-issues RequestGuildRoster() (up to MAX_RETRIES = 5) instead of silently producing an empty roster.
  • Member entry shape extended additively — Entries now include name, rankIndex, and rankName alongside the existing fields. Legacy rank is retained as an alias for rankName so MINOR=1 consumers are unaffected. rankIndex unlocks rank-based peer filtering that custom rank labels can't reliably support.

v2.0.1 — Ships the missing DeltaSyncChannel.lua. v2.0.0 added the integration scaffolding for an optional CHANNEL transport (SendChatMessage send + CHAT_MSG_CHANNEL receive, for working around WoW Classic's silent drop of CHAT_MSG_ADDON on custom channel numbers), but the implementation file itself never made it into the package — any embedder that set channelModule.enabled = true in their Initialize config hit a defensive error at boot. v2.0.1 fixes that.

  • New file DeltaSyncChannel.lua — optional ~160 LOC module loaded after DeltaSync.lua in your TOC. Adds InitChannelModule() (CHAT_MSG_CHANNEL frame setup with self-filter and prefix dispatch into the existing OnComm_* handlers) and SendViaChannel(prefix, body, target) (raw SendChatMessage transport, with a 255-byte cap warning since custom channels don't get AceComm's chunking).
  • Library revision bumped to MINOR 8 — Embedders whose private copies were on MINOR 7 (consumer addons that had been carrying their own DeltaSyncChannel.lua to work around the missing file) will now be overridden by the packaged MINOR 8 lib via LibStub, and can drop their embedded copies.
  • Compatibility — No API or wire-format changes from v2.0.0. Embedders using only GUILD/PARTY/RAID/WHISPER distribution don't need to load the new file at all and see no behavior change.

v2.0.0 — Major release folding in the matured sync engine from TOGProfessionMaster and splitting the guild-roster cache into its own companion library.

  • GuildCache extracted into GuildCache-1.0 — Roster and name-normalization methods moved from the DeltaSync handle to their own LibStub library at Libs/GuildCache-1.0/. Other addons can now embed just GuildCache without the full sync stack. This is a breaking change: calls like DeltaSync:NormalizeName(x) become LibStub("GuildCache-1.0"):NormalizeName(x).
  • aceAddon is now a required Initialize() config key — DeltaSync routes sends through your AceAddon's SendCommMessage instead of embedding AceComm into itself. This keeps throttling and CRC protection wrapping the correct send path. Existing consumers need a one-line addition: pass your AceAddon:NewAddon(...) handle as config.aceAddon.
  • AceCommQueue moved from library-side to host-side — Your addon embeds AceCommQueue into its own AceAddon handle now. One line: LibStub("AceCommQueue-1.0"):Embed(MyAddon).
  • Cross-realm name normalization fixed — Connected-realm peers whose name arrives with a foreign realm suffix are no longer silently rewritten to the local realm. Their per-realm data stays separate where it should.
  • New debug APIs — lib:DebugStatus() prints a diagnostic block showing namespace, library revision, registered prefixes, and wiring state. Useful when sends are silently dropping and you need to confirm which config key is missing.

v1.0.0 — First stable release. P2P session hardening, integrity checks on every channel, and a whisper-online guard so offline members stop eating send slots.

  • DELIVERY_TIMEOUT added so a peer that accepts a sync-request and then goes silent no longer pins an inbound session slot forever
  • All seven channel types (not just DATA) now use the CRC + stop-marker wire format
  • Whisper sends now skip guild members the roster knows are offline
  • Several P2P state-machine edge cases fixed where late messages could mutate unrelated sessions

Bug Reports & Feedback

Found a bug or have a suggestion? Reach out on Discord.

Credits

  • Pimptasty — Author

Special Thanks:

  • The Old Gods guild community for real-world testing via TOGBankClassic and TOGProfessionMaster
  • Ace3 library maintainers for the excellent framework
  • AceCommQueue-1.0 for solving the chunk-interleave CRC problem

License

DeltaSync is open-source software. See the LICENSE file for details.

You must be logged in to leave a comment.

Comments (0)