DeltaSync-v4.0.3
What's new
DeltaSync Changelog
[v4.0.3] (2026-08-03) - A refused send is no longer invisible (MINOR 17)
Adopts the integration contract AceCommQueue-1.0's README sets out for "a library sitting between a host addon and this queue" — which is exactly DeltaSync's shape.
Bug Fixes
- DeltaSync never checked whether its sends actually went out.
SendMessagecalledaceAddon:SendCommMessage(prefix, message, distribution, target, priority)with no delivery callback. WoW silently discards addon messages under congestion, and AceComm-3.0 forwards only ChatThrottleLib'sdidSendboolean — so a refused message simply vanished. With no callback supplied, AceCommQueue-1.0 reports the refusal itself throughgeterrorhandler(): correct, but it attributes the failure to the comm layer rather than to DeltaSync, and neitherDebugStatusnor the host had any structured way to see it. For a sync library that is the worst possible blind spot — a refused HANDSHAKE or DATA send stalls a P2P session until the 180-second delivery watchdog reclaims it, with nothing anywhere saying why. Location:DeltaSync.lua. - A partially-refused multipart send was recorded as delivered and failed. Only reachable in the unqueued shape, and worth spelling out because it is exactly the trap the contract warns about: when the host has not embedded AceCommQueue the callback fires once per chunk, so a 5-chunk send whose 3rd chunk is refused still ends with a final chunk reporting
true. Counting that as success recorded one message as both outcomes. A partial multipart stream reassembles into a corrupt payload on the receiver, so the only correct reading is that it did not arrive. Found by tracing the unqueued path while verifying the fix above, not by the first draft of it. Location:DeltaSync.lua.
New Features
- Delivery verdicts are recorded and surfaced.
host.sendsDelivered,host.sendFailures, andhost.lastSendFailure({ prefix, channelType, distribution, target, bytes, at }). A refusal is logged as a plain statement — "[NOT DELIVERED] … the message did NOT arrive" — rather than a soft warning, because that is what it means. Location:DeltaSync.lua. - A send that was never attempted is counted separately, not lost.
host.sendsNotAttemptedandhost.lastSendNotAttempted. Thenilverdict covers three situations — the host's own wrapper suppressed it, the queue rejected a bad argument, or the send raised — and an earlier draft of this release recorded none of them, so such a send simply vanished from the counters. Raised by AceCommQueue-1.0's maintainer while reviewing this work. Location:DeltaSync.lua.nildeliberately does NOT feedsendFailures. A suppression is one of the host's own wrappers working exactly as designed (a raid guard, say); counting it as a failure would show an addon a forever-climbing error count for behaving correctly. Trading a blind spot for a false reading would have been worse than the blind spot.- The optional 5th callback argument (
reason), shipped in AceCommQueue-1.0 v1.0.5, is read nil-guarded and decides which bucket anilverdict lands in:"rejected"(the queue refused the call — bad prefix, nil text, unknown priority) and"error"(the send raised) mean the message did not arrive. They are counted assendFailuresand fireonSendFailed, exactly like a refusal, and are logged as a defect — DeltaSync generates its own prefix and validates distribution and priority at config time, so"rejected"should be impossible."suppressed"is the host's own wrapper working as designed. It stays out of the failure count.- So the rule is
reason ~= "suppressed", notdelivered == false— which would miss the first two entirely. That correction came from AceCommQueue's maintainer after reviewing our first cut, where all threenilcases were filed together as benign. - Against a queue too old to supply a reason the send is still counted as not-attempted, and the log says the reason was not reported rather than inventing one. Both paths are covered by specs.
host.lastSendFailurenow also carriesreasonwhen the queue supplies one.
config.onSendFailed(info)— fires when the client refuses a send, so a host can re-send, degrade, or tell the user instead of assuming delivery. Optional; omitting it changes nothing. Location:DeltaSync.lua.DebugStatusreports all three delivery counters (sends: delivered=N refused=N not-attempted=N), the last refusal, the last not-attempted send with its reason, and whether the host embedded AceCommQueue at all. The last one reads__AceCommQueue_embedded, which AceCommQueue documents as part of its contract rather than an internal. It is worth showing because an unqueued host is precisely the one exposed to the chunk-interleaving corruption the queue exists to prevent — and because it determines how the delivery callback behaves. Location:DeltaSync.lua.
Known Interaction (not fixed in this release)
P2PSession's 180sDELIVERY_TIMEOUTwas sized against pre-v1.0.5 AceCommQueue behaviour. Its comment justified the number with an observed ~70s worst-case queue drain at 3 concurrent sends. AceCommQueue-1.0 v1.0.5 now retries a refused message with a doubling backoff (1+2+4 = ~7s) and holds its queue for the duration — deliberately, so ordering survives and a refusing channel is not hammered. Under sustained refusal that compounds: roughly 16 refused messages on one key adds ~112s to the 70s baseline and reaches the watchdog. Raised by AceCommQueue's maintainer, who marked the 7s[LIB-CLAIM]and the interaction[READ]— they have not run our P2P suite against it, and neither have we under real load. The number has deliberately not been re-tuned: the right value needs a real measurement under the new behaviour, and guessing a larger one would only move the cliff. The comment atP2PSession.luanow records the caveat instead of citing a measurement that no longer holds. The better fix is to stop waiting for the watchdog at all — MINOR 17 gives a delivery verdict per send, so a session whose QUERY or DATA was refused could fail immediately rather than 180s later. That is a behaviour change and is not in this release.
Compatibility
- Additive. No API removed, renamed or reordered; no wire-format change; sending behaviour itself is unchanged. A consumer that does nothing gains the diagnostics for free and needs no code change.
- Both AceCommQueue callback shapes are handled, per its documented contract: one terminal callback carrying the whole-message verdict when the host embedded the queue, one callback per chunk when it did not. A check written for one shape misbehaves under the other, so the suite models both —
Tests/comms_spec.lua"delivery verdicts" (16 specs) proves a refused 5-chunk send is counted exactly once either way, that a partial refusal is never also counted as delivered, and that all three verdict states are handled:true(accepted),false(refused — it did not arrive), andnil(never attempted), which gets its own counter rather than being folded into either. Thenilpath is covered both with and without the optionalreasonargument, so the behaviour is pinned against the AceCommQueue that ships today and the one that supplies it. - Already compliant with the contract's other requirement: DeltaSync never calls
SetRetryPolicyorSetDebug. Those live on the shared LibStub table and are session-global, so a library in the middle changing them would alter behaviour for unrelated addons that never asked. Retry policy belongs to the host.
Documentation
- The README is now written for any consumer, not just the addons that happen to embed it today. Three places still claimed MINOR 16 / v4.0.2; the adoption checklist opened by assuming the reader was already a consumer, with no pointer to Quick start for a new one; and the
RegisterDebugCategoryexample used a bank-specific category ("BANK"/ bag scans / mail parsing) that reads as if the library knows something about inventories. It does not — it moves opaque tables between guild members and calls you back, and the README now says so where a new consumer will actually see it. Location:README.md. - Feature detection is now a table rather than a sentence, covering MINOR 12 → 17 (the previous list stopped at 15, so nothing pointed at
MakeHashEntry,RegisterDebugCategory,strictKeys, or this release's delivery verdicts). It also states the reason detection matters at all: the highest MINOR present in the client serves every consumer, so the revision you shipped against is not necessarily the one you get. Location:README.md. - The "behaviour changes you get whether you adopt anything or not" section now carries v4.0.3 as well as v4.0.2 — delivery accounting populating with no config change, a refused send being logged as a plain statement (existing addons broadcasting on
GUILDwhile guildless will start seeing something that was always happening), and the deliberately un-retuned 180s delivery watchdog flagged for anyone driving heavy P2P traffic. Location:README.md.
LibStub MINOR
- Bumped from 16 → 17. Feature-detect with
DS.MINOR >= 17, orif host.lastSendFailure ~= nil or host.sendsDelivered then.
[v4.0.2] (2026-08-03) - Offline test suite, five silent-failure fixes, hash revision 2 (MINOR 16)
Consumer Requirements (TOGBankClassic docs/LIBRARY_CONTRACTS.md)
TOGBankClassic audited all four TOG libraries before migrating onto them and filed LIBREQ-* tickets. Every DeltaSync-side blocker it named is addressed here; the ticket ids are kept so both repos can cross-reference.
LIBREQ-DS-001(timers) — fixed; see Bug Fixes below. Their acceptance criterion ("a spec asserting a cancelled timer's callback does not run") is met bysmoke_spec.lua.LIBREQ-DS-004(P2P-024, stale safety release) — fixed; see Bug Fixes below.LIBREQ-DS-005(silentDefaultKeyFuncaddress fallback) — fixed; see Bug Fixes below.LIBREQ-DS-006(keyFields⊇keyFunc) — fixed; see Bug Fixes below.LIBREQ-ALL-005(check prefix registration) — implemented in DeltaSync rather than the host. The ticket proposed AceCommQueue or the host on the grounds that AceComm discards the result, but DeltaSync callsC_ChatInfo.RegisterAddonMessagePrefixitself inRegisterCommChannels, so it is the right place. See Improvements.LIBREQ-ALL-002(offline testability) — met: the suite runs on the shared WoWAPITesting harness, and the gaps it needs are written up inTests/HARNESS_CONTRACT.mdrather than stubbed privately. Their item 5 ("a test suite should precede the migration") is likewise met.- §3.10 (cyclic
ObjectsEqual) — fixed; a self-referential record hung the client. LIBREQ-DS-002(prefix/protocol transition) — no library change needed. Their recommended option C (one host, requests riding DATA with a type discriminator) is already supported byRegisterLeafType, which routes on a payloadtypefield; option B works too via a second host. This is TOGBank's sequencing decision, not a library gap.LIBREQ-DS-003(logger ownership) — still open, needs a decision. Their preference — DeltaSync accepting an injected logger rather than owning a chat frame — is the cleaner boundary and is not yet implemented.- §3.10 (duplicate keys within
newArray) — acknowledged, not yet addressed.
Bug Fixes
LIBREQ-DS-004/P2P-024— a completed send's safety timer released a LATER send's slot.TryAcquireSendSlotarmed an unconditionalC_Timer.Afterwhose comment claimed it fired "ifReleaseSendSlotwas never called" — it fired either way. When a send completed normally the real release ran, then the stale timer fired later and decremented a slot belonging to a different, still-in-flight send from the same requester. That is an over-release rather than an underflow, so the> 0guard never caught it andMAX_ACTIVE_SENDScould be quietly exceeded under sustained load. Each safety timer is now aNewTimerbound to the acquisition that scheduled it, retired FIFO by the matching release — so the timers still guarding live sends keep their own deadlines and a genuinely leaked slot is still reclaimed on schedule, not early. The inaccurate comment is corrected too. Location:P2PSession.lua.LIBREQ-DS-005— the default key function fell back to a table ADDRESS in silence. With nooptions.keyFunc, records carrying none ofid/ID/key/namewere keyed bytostring(obj). Addresses are stable neither across sessions nor between clients, so every record keys uniquely, every diff reports everything added and everything removed, and the consumer full-resyncs forever — with no error. Positional records ({id, count, suffix}, exactly what TOGBank's V2 rework uses) hit it immediately. The fallback now reports itself once per host through both the debug log and chat, andoptions.strictKeysmakes it an error instead. Location:DeltaOperations.lua.LIBREQ-DS-006—keyFieldssilently had to cover every fieldkeyFuncreads.ComputeArrayDeltareduces a removed entry to justkeyFields, butApplyArrayDeltathen callskeyFuncon that reduced entry. If the reduction dropped a field the key depends on — akeyFuncover(id, suffix)withkeyFields = {"id"}— the key computed on apply differed from the one computed on compute, the removal matched nothing, and deleted items lingered forever with no error. Compute now compares the reduced entry's key against the full record's and reports a mismatch (or errors understrictKeys). Location:DeltaOperations.lua.- A cyclic record hung the client.
ObjectsEqualrecursed with no cycle detection, so a self-referential table recursed until the stack blew. It now carries a seen-pair guard. Location:DeltaOperations.lua. - Timer cancellation never worked —
C_Timer.Afterreturns nothing, so every:Cancel()inP2PSession.luawas a silent no-op. Four timers were created withC_Timer.Afterand their result stored for later cancellation, but onlyC_Timer.NewTimerreturns a handle carrying:Cancel(). The stored value was alwaysnil, so each cancel sat harmlessly behind anif timer thenguard and did nothing — which reads as correct code right up until you look at what the API actually returns. All four now useC_Timer.NewTimer. Location:P2PSession.lua.- The user-visible half of this: extending the offer collect window did not extend it.
BeginCollectWindowon an already-open window is meant to push the deadline back so late-arriving offers still count. The failed cancel meant the original timer survived and a second was stacked on top, soDispatch()fired at the original deadline anyway — cutting the window short and discarding offers that arrived in the extension — and 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, this cost real offers and doubled the dispatch work. - The other three (dispatch timeout, delivery watchdog, retry cycle) were harmless in effect — every callback re-checks live session state and no-ops — but each leaked a timer that stayed queued for its full duration (up to 180s) after the session it belonged to had completed.
- The user-visible half of this: extending the offer collect window did not extend it.
DeltaOperations.lua's load-order guard could never fire. It resolved the library withLibStub:GetLibrary(MAJOR)— the non-silent form, which raises LibStub's own generic "Cannot find a library instance" before theif not libcheck is reached. An embedder with a bad TOC order got the vague message instead of this file's actionable one. Now uses the silent form, matching the other four modules. Location:DeltaOperations.lua.
Testing
- Added a full offline unit-test suite: 509 specs, 99.54% line coverage across all six library files, run locally with a Lua 5.1 interpreter in milliseconds — no game client, no LuaRocks, no C modules. Built on the shared WoWAPITesting harness, added as a git submodule at
Tests/wowapi. Location:Tests/. - The suite loads the REAL AceSerializer-3.0 and the REAL LibGuildRoster-1.0 from the sibling AddOns folders rather than stubbing them — the exact code that ships to players, so an integration bug cannot hide behind a lookalike.
- A loopback AceComm network (
Tests/env_delta.lua) lets two or three DeltaSync hosts in one Lua state genuinely talk to each other, sointegration_spec.luadrives complete broadcast → offer → handshake → deliver → apply cycles and asserts on the data that lands in the receiver's model. It also swaps per-character LibGuildRoster state around each peer's turn to run, which is what makes a true cross-guild RosterSync exchange testable. - The environment models the WoW API faithfully rather than conveniently.
C_Timer.Afterreturns nothing (onlyNewTimeryields a handle) and a GUILD addon message is echoed back to its own sender. Both are deliberate: the first is what surfaced the timer defect above — a convenience stub returning a fake handle would have made every broken cancel pass — and the second is what exercises the self-ignore guard opening everyOnComm_handler. Tests/coverage.luareports exact line coverage with no dependencies, taking the executable-line set from Lua 5.1 bytecode debug info rather than guessing from source text. Extended over GuildRoster's copy to accept multiple targets so the whole library is measured in one spec run.- The ten lines left uncovered are unreachable by construction (a
returnaftererror(), guards whose callers already guarantee the value, a dead branch shadowed byP2PSession.lua's override oflib:InitP2P) and are enumerated individually inTests/HARNESS_CONTRACT.mdrather than papered over.
New Features
- Hash revision 2 fixes the
ComputeHashtype collision, and ships alongside revision 1 so mixed-build guilds keep working — no flag day, nothing to coordinate. Revision 1 renders a number withtostring()and a string as itself, so{v = 1}and{v = "1"}produce identical hashes, as dotrueand"true". A consumer storing a value as text therefore looks unchanged to a peer storing it as a number, and the sync is skipped. Revision 2 prefixes each scalar with its type (n:1vss:1). Locations:DeltaSync.lua,P2PSession.lua.lib:ComputeHashV2(value)— the corrected hash.lib:ComputeHashis unchanged and now explicitly frozen: its output is compared between clients, so altering it in place would make upgraded peers disagree with un-upgraded ones forever.lib:MakeHashEntry(value, updatedAt)— builds a P2P hash-list entry carrying both revisions (hash+hashV2). This is the recommended way to populategetMyHashes, and it is what makes the rollout free: consumers never name a revision, so retiring revision 1 later changes this one function and no call sites.P2PSessioncompares on the highest revision BOTH ends advertise. A peer on an older build sends onlyhash, and that absence is the signal to fall back to revision 1 — so the pair still agrees instead of reading a phantom difference and offering each other data forever. Two upgraded peers get the collision-free comparison. A guild can therefore upgrade one player at a time, with the fix switching itself on per pair.- The VERSION channel gained the same optional carriage, since that comparison belongs to the consumer:
BroadcastVersion(version, hash, distribution, priority, hashV2)sends both andonVersionReceived(sender, version, hash, hashV2)delivers both. Purely additive — existing 4-argument callers and 3-argument callbacks are unaffected, andhashV2is simply nil from older peers.peerStatesrecords it too. - Verified scope first: the library never feeds
ComputeHashonto the wire — only consumers do. Two do today (FGI'sBridge:ItemHash→GetMyHashes, and TOGProfessionMaster'sHashManager), and neither looks bitten at its current call sites (FGItonumber()ssetAtbefore hashing; TOGPM hashes numeric values under string keys). The defect is latent, which is exactly why it had to be fixed without forcing churn on either addon. - Covered end to end:
integration_spec.luadrives real hosts on both builds against each other — old↔new syncing in both directions, a mixed pair holding identical content staying completely silent (the resync storm this design exists to prevent), and the type-only collision being caught once both ends are upgraded. That last pair of tests holdsupdatedAtidentical on purpose, so revision 1 genuinely cannot tell the two states apart and the test can only pass because revision 2 can.
Deprecations
config.hashStrategyis deprecated and now inert — commented out, pending removal after ~2026-08-24. It was stored on the host and then read by nothing: no code path in any of the six files ever branched on it, so"deep"and"shallow"behaved identically while the API docs described them as a choice. A knob that documents an effect it does not have is worse than no knob, because a consumer sets it and believes it took. Location:DeltaSync.lua.- Passing it remains harmless — it is simply ignored, exactly as it always effectively was. No consumer needs to change anything.
- Commented rather than deleted so it can come back cheaply if a consumer turns out to be reading
host.hashStrategy. Nothing in this workspace does — checked TOGProfessionMaster, PersonalShopper, FGI and TOGBankClassic. It will be removed outright in a later release. config_spec.luapins the deprecation from both sides: the field must be absent, and passing it must not break initialization. If someone re-adds the assignment without wiring it to real behaviour, the first assertion fails.
Improvements
LIBREQ-DS-003— logging ownership is now a choice, in both directions. A consuming addon can either hand logging to DeltaSync or take it over completely:config.logger— the host takes over. DeltaSync forwards every debug line's raw arguments (category, tag, format, ...) and stops filtering, buffering and claiming a chat tab entirely. Accepts a plain function or any object with a:Debugmethod, so a host passes its existing module straight in — TOGBankClassic'sOutput:Debug(fmt, ...)already has the identical call shape, making the integration one line.host:RegisterDebugCategory(name, tags)— DeltaSync keeps it, and the host adds its own categories to the shared system rather than building a second one. Registered categories are indistinguishable from the built-ins (INIT,COMMS,DELTA,P2P, …): same opt-out filtering, same[CATEGORY.TAG]prefixing, same SavedVariables persistence. Per-host, so two consumers never see each other's.- The recommendation is the second one for most consumers. DeltaSync is shared across roughly twenty addons; if every consumer injects its own logger, twenty addons each build a debug subsystem and the library's diagnosability varies by whoever you are debugging. One shared tab means "screenshot the DeltaSync tab" means the same thing everywhere. Inject a logger you already own for other reasons; don't build one in order to inject it.
config.onDebugMessage(message, category, tag)— a tap, not a replacement. Fires for every message DeltaSync logs, alongside its own tab and buffer, so a host can persist or export the library's diagnostics without DeltaSync owning retention. DeltaSync's buffer is deliberately session-only, in-memory and capped at 1000, and it never writes messages to SavedVariables: retention costs the host's SV file, so the host sets the policy. Receives the structured category and tag as well as the rendered text, never fires for a message the host's own filtering suppressed, and routes a throwing sink togeterrorhandler()rather than swallowing it — a broken sink must not take the library's logging down with it, and must not disappear either.- Location:
DeltaSync.lua.
- Duplicate keys within
newArrayno longer let array order decide the outcome (TOGBankClassic §3.10). Two records sharing a key emitted two entries for one target, and on apply the second overwrote the first — so the surviving record depended on the order the host happened to build its array in. Deterministic for one client, but two clients whose source order differs (an array built frompairsover a map) kept different records, hashed differently, and offered each other data forever.ComputeArrayDeltanow reports it once, keeps the last occurrence (exactly what apply already did, so no behaviour changes on upgrade) and emits one entry instead of two — the applied result is identical and the payload is smaller.options.strictKeysmakes it an error. Detection costs one table and no extrakeyFunccalls: keys are computed once in a pre-pass and reused by the main loop. Location:DeltaOperations.lua. LIBREQ-ALL-005— a prefix the client refuses to register is now reported.C_ChatInfo.RegisterAddonMessagePrefixreturns false once the client's registered-prefix cap is reached, and AceComm discards that result; the symptom is messages that never arrive on that prefix, with no error and no warning. DeltaSync now checks it, reports the dead channel by name through the debug log and chat, and records it inhost.prefixRegistrationFailedforDebugStatus. Only an explicitfalsecounts — some clients return nil, and treating that as failure would cry wolf on every register. Location:DeltaSync.lua.options.strictKeys(new, opt-in) turns both keying hazards above into errors instead of warnings, for consumers that would rather fail loudly at development time. Location:DeltaOperations.lua.
Tooling
- luacheck now runs clean: 0 warnings, 0 errors across 24 files. Installed luacheck 1.2.0 into the local Lua 5.1 tree and actually ran it rather than shipping an unexecuted config. It found a genuine config gap of ours —
time(WoW's bareos.timeequivalent, used asDeltaSyncRoster'sGetServerTimefallback) was missing fromread_globals, so a valid call was reported undefined. Real code findings fixed: three redundantlocal x = nilinitializers inlib:Debug, an unusedMINORconstant inDeltaOperations.lua(only the one atDeltaSync.lua'sNewLibrarycall site is meaningful), and three unused loop variables.unused_argsis off — WoW callbacks are positional, so namingevent/language/flagsto reach a later argument is required, not a defect. Whitespace-only codes are ignored, documented, so real findings aren't buried under ~70 cosmetic ones. Locations:.luacheckrc,DeltaSync.lua,DeltaOperations.lua,P2PSession.lua. - The suite is verified under real busted too, not just the bundled runner: 424 successes / 0 failures / 0 errors, matching
run.luaexactly. - Fixed a shared-harness bug that affects every TOG addon.
busted_config.lua'sexclude-pattern = "wowapi"is a no-op: busted matches that pattern against a file's basename, never its path, so a directory name can never match. Runningbustedfrom any consuming addon's root therefore collected the harness's own self-tests and ran them from the wrong working directory, where their relative fixture paths don't resolve — a spurious error in every consumer's suite. Worked around locally withconfig.default.recursive = false(an addon's specs live directly inTests/; the harness's are inTests/wowapi/spec/), and written up inTests/HARNESS_CONTRACT.mdas a one-line upstream fix. Location:.busted. DeltaSync.code-workspacewas disabling Lua'sbasic,string,tableandmathbuiltins, which do exist in WoW — that is why.luarc.jsonhad to re-declaretable,string,pairsandtypeas "globals" to undo it, and why the test suite'sassert/require/loadfilewere reported undefined (workspace settings override.luarc.json). Those four are re-enabled;io/os/package/debug/utf8stay disabled since they genuinely are absent from the client, with the names the offline suite needs declared as globals instead. Location:DeltaSync.code-workspace,.luarc.json.
Packaging
Testsadded to.pkgmetaignoreso the suite and its submodule never reach players..pkgmetaignore list rewritten to the packager's documented syntax rules — folder entries as bare names rather than with trailing slashes, single-star repo-relative globs rather than recursive**/, and no dotfile entries (the packager prunes those unconditionally, so listing them implied coverage they were not providing).Testsadded, which is the entry that actually mattered: without it the whole offline suite would have shipped to players. Location:.pkgmeta.- Correction: an earlier draft of this entry claimed
docsandtextures"were shipping in the zip" as a result of the old syntax. That was wrong, and it was asserted from reading the packager's rules rather than from inspecting a built artifact. Downloading the v4.0.1 release shows it contained exactly the same 11 files as v4.0.2 — the six library files,DeltaSync.toc,README.md,CHANGELOG.md,LICENSE— with nodocsortexturesdirectory. The rewrite is still correct and still worth having (the syntax now matches what the packager documents, andTestsgenuinely needed excluding), but it fixed a latent correctness problem, not an observed one. The v4.0.2 GitHub release body carries the original wording, since it was generated from this file at tag time.
- Correction: an earlier draft of this entry claimed
- Interface versions refreshed to the current build per flavour —
11509, 20506, 30405, 38000, 40402, 50504, 120007. Drops entries superseded by a newer build of the same flavour (11507,20505,50503,50502,110207,120005); every flavour DeltaSync shipped for is still covered. Location:DeltaSync.toc.
Documentation
- README gains developer-facing sections: a protocol walkthrough ("How a sync actually happens") covering the three things integrators most often get wrong, a Testing section, and a symptom→cause troubleshooting table.
Tests/HARNESS_CONTRACT.mddocuments what the shared harness should absorb (the faithfulC_Timercontract above all), written so it can be applied upstream rather than re-derived.
LibStub MINOR
- Bumped from 15 → 16.
P2PSession.luaandDeltaOperations.luaboth changed; embedders on MINOR 16 get working timer cancellation. No wire-format or API change — a MINOR 16 host and a MINOR 15 peer interoperate exactly as before.
[v4.0.1] (2026-07-28) - Classic Era interface bump to 11509
Packaging
- Classic Era interface level bumped
11508 → 11509inDeltaSync.tocso the library isn't flagged out-of-date on the current Classic Era client. The remaining entries in the## Interface:list (11507, 20505, 30405, 38000, 40402, 50503, 50502, 120007, 110207, 120005) are unchanged, so every other flavor DeltaSync ships for is unaffected. Location:DeltaSync.toc.
LibStub MINOR
- Unchanged at 15. No library
.luafile changed in this release — it is a TOC-only bump, so embedders see no new revision and no LibStub upgrade churn.
[v4.0.0] (2026-07-01) - Multi-host: DeltaSync is no longer a singleton (MINOR 15)
New Features
lib:NewHost(config)— isolated per-host instances. DeltaSync was a singleton:LibStub:NewLibraryreturns one shared table, andInitializewrote every per-host field (namespace, prefixes, callbacks, peerStates, localState, p2p, rosterSync, guildMode, leafHandlers, aceAddon) onto it — so two consuming addons in one client clobbered each other, last-Initialize-wins.NewHostreturns a fully isolated host object (setmetatable({}, { __index = lib })) that owns its own copy of all that state plus its ownRegisterCommregistrations on its ownconfig.aceAddon. Multiple consumers now coexist without cross-talk: distinct namespaces → distinct prefixes → independent sync. Every instance method (BroadcastVersion,RequestData,SendData,BroadcastItemHashes,InitP2P,InitRosterSync,InitGuildMode,DebugStatus, the delta ops, …) is invoked on the returned host.configshape is identical toInitialize. Location:DeltaSync.lua.lib.MINORis now actually exposed on the handle. Prior versions documented feature-detection viaLibStub("DeltaSync-1.0").MINOR >= N, but the field was never set (MINOR lived only in LibStub's internalminorstable). It's now assigned on the handle, soDS.NewHost and DS.MINOR >= 15works as documented. Location:DeltaSync.lua.
Backward Compatibility
lib:Initialize(config)is retained as sugar over the multi-host core. It initializes onto the sharedlibtable itself — an implicit "default host" — so a single un-migrated consumer is byte-compatible with pre-v4.0.0 behaviour (sameDS:Method()/DS.p2p:OnItemCompleted()calls, same wire traffic). Caveat: there is only one default-host slot, so two consumers both callingInitializestill clobber each other. The rule: migrate all but at most one consumer toNewHost; a client is fully clobber-free once every consumer is onNewHost. No breaking change to the documented API —NewHostis additive andInitializeis unchanged for a lone consumer; the major bump signals the architectural shift.
Internal Architecture
- P2PSession / RosterSync / GuildMode moved from file-local singletons to persistent class tables. Each was a
local X = lib.xxxsingleton whose methods and (for P2P) timer closures captured that one shared table. They are now persistent class tables kept onlibacross LibStub upgrades (lib._P2PClass,lib._RosterClass,lib._GuildModeClass), and each host owns its own instance (host.p2p/host.rosterSync/host.guildMode) carrying a_hostback-reference. All P2P timer/retry/catch-up closures now capture the per-host instance instead of the global, and every internal send (SendHandshake/SendHashOffer/BroadcastItemHashes) and debug line routes throughself._host— so each addon's P2P traffic and debug log stay on its own host. Storing the classes onlib(not a fresh file-local each load) means a hot upgrade redefines methods on the same tables and live instances pick them up through their metatable. Locations:P2PSession.lua,DeltaSyncRoster.lua,DeltaSyncGuildMode.lua. - Per-host optional-module sentinels.
NewHost/Initializeseedhost.p2p/host.rosterSync/host.guildModetofalse(an own field) so the core seams (OnComm_OFFER/OnComm_HANDSHAKE'sif self.p2p,SendMessage/_GuildModeInbound'sself.guildMode) read this host's own falsy value and never fall through__indexto the default host's instance. TheInit*methods replace the sentinel with a real instance viarawget.GetCommStatsswitched to a truthiness check so the sentinel isn't misreported as enabled. Location:DeltaSync.lua. - Per-host leaf router.
host.leafHandlersis a fresh table per host (never the sharedlib.leafHandlers), so two hosts both claiming the same leaf type (e.g."roster") don't collide. Location:DeltaSync.lua.
Wire Format / Protocol
- Unchanged. No new prefix, no envelope change; the 7-channel-per-addon budget is untouched. A v4.0.0 host and a pre-v4 peer interoperate exactly as before — this is purely a client-side isolation change.
Migration Notes for Consumers
- All consumers should migrate to
NewHost: replacelocal DS = LibStub("DeltaSync-1.0"); DS:Initialize{...}withself.dsHost = DS:NewHost{...}and call everything on the held handle —DS:Method(...)→self.dsHost:Method(...),DS.p2p:OnItemCompleted(...)→self.dsHost.p2p:OnItemCompleted(...). Feature-detect withif DS.NewHost and DS.MINOR >= 15 then. - Ordering: because
Initializestill works, consumers can migrate one at a time; a client is only guaranteed clobber-free once every DeltaSync consumer in it is onNewHost(or all but one are). - Non-consumers / untouched call sites: a lone consumer that keeps calling
Initializeneeds no changes.
LibStub MINOR
- Bumped from 14 → 15.
Tooling
- Added
rawgetto.luarc.json's global whitelist (used by the per-host instance guards).
[v3.2.1] (2026-07-01) - Send APIs return serialized byte size for host logging (MINOR 14)
Improvements
lib:BroadcastDataandlib:BroadcastItemHashesnow return the serialized payload size (bytes) as a second value alongside the existingokboolean — the same#messagemetric the receive path already reports aslen. Lets a host 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 earlyreturn falsefailure paths still return a single value.BroadcastDataguards the size read (message and #message or 0);BroadcastItemHashesreads#messagedirectly since its early-out already guarantees a non-nil payload. Location:DeltaSync.lua.
LibStub MINOR
- Bumped from 13 → 14.
[v3.2.0] (2026-06-29) - Guild-mode: WHISPER→GUILD directed routing for whisper-broken servers (MINOR 13)
New Features
lib:InitGuildMode(config)/lib:SetGuildMode(enabled)/lib:IsGuildMode()— opt-in "guild-mode". New module DeltaSyncGuildMode.lua (loaded last) for private/emulated cores — notably Whitemane — that don't deliver addon messages over WHISPER (CHAT_MSG_ADDONnever fires for whispers), which silently breaks every directed channel. When the user enables the toggle, the five directed channels (QUERY, RESPONSE, DELTA, the directed OFFER reply, HANDSHAKE) are rerouted from WHISPER to GUILD.config.enabledapplies a host-persisted toggle at init;config.onChanged(bool)fires on every flip so the host can persist + refresh its menu. Inert untilInitGuildModeis called — consumers that don't opt in (e.g. TOGBankClassic) are byte-identical to v3.1.0. This is a USER toggle, not server detection: the host owns the settings UI and persistence (the library ships no GUI/SavedVariables/slash commands). Location:DeltaSyncGuildMode.lua.
Wire Format / Protocol
- No new prefix. Guild-mode reuses the existing five directed channels; only their
distributionflips WHISPER→GUILD. The 7-channel-per-addon budget is unchanged. - Recipient stamp (additive, out-of-band). GUILD is a broadcast — every guild member receives and processes it on the shared addon channel — so each directed GUILD send is prepended with
\029<recipient>\029, outside the existing<payload>\030<checksum>\031ENDenvelope. Every receiver drops a message stamped for someone else and strips the stamp from its own, so exactly one peer acts — behaviour identical to a whisper, with no N² response storms.\029(Unit Separator) never begins an AceSerializer payload (which always starts^1), so detection is unambiguous, and the stamp survives AceComm chunking/reassembly. The recipient is realm-qualified at send time so same-named characters on different connected realms don't both match. - Bit-compatible for everyone else. With guild-mode off (the default), nothing is stamped and the directed channels stay on WHISPER — existing consumers are unchanged. A peer without the module that receives a stamped message simply fails the CRC and drops it.
Behaviour / Interactions
- RosterSync self-disables under guild-mode. RosterSync is cross-guild (it reaches members of other guilds), and GUILD cannot reach a non-guild player — on a whisper-dead server cross-guild sync is impossible regardless. So
RequestRosterSync,RS:OnRequest, andRS:OnResponseno-op while guild-mode is active, keeping undeliverable roster traffic off the shared GUILD channel. Host-registered intra-guild leaf types (e.g.cooldowns:) are not disabled — they reroute + stamp correctly. Location:DeltaSyncRoster.lua. - Offline guard generalized. The "skip a directed send to a known-offline guild member" guard now applies to guild-mode's directed GUILD sends as well as WHISPER, suppressing guild-wide broadcasts no online peer would act on. Location:
DeltaSync.lua. - Cost note. The stamp makes non-recipients drop early, but GUILD still delivers the bytes to every member, each of whom fully reassembles a multi-fragment payload before dropping it (the drop is post-reassembly — AceComm exposes no per-fragment hook). Guild-mode buys correctness, not bandwidth; hosts should keep directed payloads (esp. DELTA/RESPONSE) small under guild-mode.
Migration Notes for Consumers
- Hosts wanting guild-mode: call
lib:InitGuildMode{ enabled = sv.guildMode, onChanged = function(on) sv.guildMode = on end }once afterInitialize, and wire a settings toggle tolib:SetGuildMode(checked)/lib:IsGuildMode(). The host owns the checkbox and its persistence. Feature-detect withif lib.InitGuildMode then. - Non-consumers: no change, no action — the module is inert unless
InitGuildModeis called. - Detecting guild-mode:
LibStub("DeltaSync-1.0").MINOR >= 13, or feature-detectlib.InitGuildMode.
Documentation & Packaging
- Added a developer-facing
README.mdat the repo root — what DeltaSync is, dependencies, embedding (.pkgmetaexternals + TOC + the 6-file load order), quick start, the 7-channel table, a grouped API reference (including RosterSync and Guild-mode), and the package-version-vs-MINOR scheme. It ships in the package and is self-contained: thedocs/folder andCLAUDE.mdare excluded from packaging, so the README links only to files that ship (CHANGELOG.md,LICENSE). - Added an MIT
LICENSEat the repo root (the project description already referenced one). BothREADME.mdandLICENSEship in the zip — neither is in the.pkgmetaignore list.
LibStub MINOR
- Bumped from 12 → 13.
[v3.1.0] (2026-06-04) - RosterSync: cross-guild sister-roster sharing (MINOR 12)
New Features
lib:InitRosterSync(config)— opt-in cross-guild roster sharing. New module DeltaSyncRoster.lua (loaded afterP2PSession.lua) lets confederated guilds share membership over the existing WHISPER QUERY/RESPONSE channels — no new prefix, no broadcast, no P2P-offer participation.config.onSisterRosterUpdated(guildKey)fires after a sister roster is applied (host persists + refreshes UI); optionalconfig.isValidPeer(name)gates whom we serve our home roster to (default accept-all; scoped to RosterSync — it never touches the P2PisValidPeer). Inert until called, so consumers that don't opt in (e.g. TOGBankClassic) are byte-identical to v3.0.0. Requires LibGuildRoster-1.0 MINOR 6+, feature-detected — disables cleanly if the sister API is absent.lib:RequestRosterSync(peerName)— the single call a consumer makes. The host's/whodiscovery finds an online sister-guild member and hands the name over; DeltaSync does the rest: whispers a QUERY carrying the requester's cached{guildKey→membershipHash}map (built fromLibGuildRoster:GetKnownRosters()+GetRosterHash()); the provider short-circuits with "no-change" when the requester is already current, else replies with the full membership + its canonicalGetHomeGuildKey()+ provenance; the requester applies it viaLibGuildRoster:SetSisterRoster(theirKey, members, meta). Single round trip, provider-authoritative key. Location:DeltaSyncRoster.lua.lib:RegisterLeafType(prefix, handlers)— generic additive leaf-type router. A small core addition: an optional module claims a leaf-keytypeprefix so its directed traffic dispatches to it (bybaseline.typeon QUERY,data.typeon RESPONSE/DATA/DELTA) before the host'sonDataRequest/onDataReceived. Aroster-typed message coexists with the host's own leaves (e.g.cooldowns:/recipes:) without either seeing the other's traffic. When no handler is registered the router is a no-op and host callbacks fire exactly as before. Location: DeltaSync.lua.
Design — division of labour
- RosterSync owns the wire and writes MEMBERSHIP (
SetSisterRoster); LibGuildRoster owns the store (membership + frozen FNV-1a32 hashing + sister API); the consumer owns PRESENCE (its/whopoll callsLibGuildRoster:MarkOnline) and PERSISTENCE (re-feedsSetSisterRosterfrom SavedVariables on login). RosterSync is membership-pure — no presence, no SavedVariables of its own. - Trust: cooperative confederation. A provider serves only its own
GetHomeGuildKey(), so it can corrupt at most its own guild's roster; the receiver additionally rejects any roster whose stampedproviderCharKeyis absent from the served members.meta(provider charKey + timestamp) is the audit trail. No crypto in this release.
Robustness
- Load-order-resilient LibGuildRoster handle.
Initialize,InitP2P, andInitRosterSyncnow re-resolve theLibGuildRoster-1.0handle at init time (post-PLAYER_LOGIN, when every addon is loaded) instead of relying solely on the file-load-timeLibStubupvalue, reassigning the shared upvalue so all call sites pick it up. The hard## Dependencies: GuildRosteralready guarantees load order for a normal install; this additionally covers an embedder that vendors DeltaSync's source without declaring the dependency, or a TOC whose dependency line is lost. Location:DeltaSync.lua,P2PSession.lua,DeltaSyncRoster.lua. ROSTERdebug category. Added toDEBUG_CATEGORYso RosterSync'slib:Debug("ROSTER", …)lines render their actual message instead of being swallowed by the no-category branch (which had reduced every line to[ns] ROSTER). Location:DeltaSync.lua.
Wire Format / Protocol
- No new prefix. RosterSync rides the existing QUERY (request) and RESPONSE (reply) channels over WHISPER, discriminated by
type == "roster". The 7-channel-per-addon budget is unchanged. roster:*leaves are directed-whisper-only and are never placed in the P2PgetMyHashesmap — keeping them out of guild-wide OFFER broadcasts is a hard isolation requirement, not an optimization.- Bit-compatible for everyone else. Existing QUERY/RESPONSE/DATA/DELTA payloads are untouched; the leaf router only intercepts payloads whose
typematches a registered prefix, and nothing registers one unless a host opts in.
Migration Notes for Consumers
- TOGProfessionMaster (RosterSync consumer): call
lib:InitRosterSync{ onSisterRosterUpdated = … }once afterInitialize, thenlib:RequestRosterSync(peerName)per online sister member found via/who. Feed presence yourself withLibGuildRoster:MarkOnline(sisterKey, names)from the same/whoresults, and persist sister rosters by re-feedingSetSisterRosterfrom SavedVariables on login. Read results viaLibGuildRoster:IsInAnyRoster/GetOnlineMembersScoped. Feature-detect withif lib.InitRosterSync then. - TOGBankClassic and other non-RosterSync consumers: no change, no action — the module is inert unless
InitRosterSyncis called. - Detecting RosterSync:
LibStub("DeltaSync-1.0").MINOR >= 12, or feature-detectlib.InitRosterSync.
Repository (not shipped to CurseForge)
- Cross-version dev-sync watcher. Added
wow-version-replication.ps1and a VS CodefolderOpentask (.vscode/tasks.json) that mirror the working copy from_classic_era_into the other installed WoW client folders (_classic_,_anniversary_,_retail_) during development, applying the.pkgmetaignore list so the targets look like the packaged release. Both are excluded from the package (.pkgmetaignores.vscode/and*.ps1/**/*.ps1— a root-level*.ps1was added alongside the existing**/*.ps1), so nothing dev-only ships to CurseForge.
LibStub MINOR
- Bumped from 11 → 12.
[v3.0.0] (2026-06-04) - GuildCache-1.0 retired; roster engine now external LibGuildRoster-1.0 (MINOR 11)
Breaking Changes
- The embedded
GuildCache-1.0LibStub library has been removed. DeltaSync no longer shipsLibs/GuildCache-1.0/GuildCache-1.0.lua, and theGuildCache-1.0LibStub MAJOR is no longer registered by DeltaSync. Any addon that resolvedLibStub("GuildCache-1.0")from DeltaSync's bundle now receivesnil. Roster tracking is now provided byLibGuildRoster-1.0— the standaloneGuildRosterCurseForge addon (CF sluglibguildroster) — declared as a required dependency that CurseForge auto-installs and WoW loads before DeltaSync. See Migration Notes below.
Changed
- DeltaSync now consumes
LibGuildRoster-1.0instead ofGuildCache-1.0for player identity (GetNormalizedPlayer), the whisper online-guard (GetMember), and the default P2PisValidPeer(IsInGuild+IsReady). The soft-dependency presence check is retained, plus per-method feature-detection (if GuildRoster and GuildRoster.GetNormalizedPlayer then), so DeltaSync still degrades gracefully when the lib is absent or an older MINOR. Location:DeltaSync.lua,P2PSession.lua. - Whisper online-guard switched from a direct
lib.guildRostertable read to theGetMember()accessor. Behavior is unchanged — skip a whisper only when the target is a known guild member who is offline; non-members and cross-realm targets return a nil member and pass through. This drops the coupling to the library's internal table name (which changed fromguildRostertoroster). Location: DeltaSync.lua:756-770. - Default
isValidPeerrewritten to preserve prior semantics. The retiredGuildCache:IsInGuildreturnedtrueon an empty/not-yet-built roster, so non-guild / early-login peers were never blocked.LibGuildRoster:IsInGuildis a strict membership check instead. The default wrapper now accepts any peer when the local player isn't in a guild or the roster isn't ready (IsReady()), and only filters once it is — preserving MINOR≤10 P2P eligibility behavior. Location: P2PSession.lua:150-165.
Bug Fixes / Robustness
playerFullNameis no longer left nil when the roster lib returns nil early.Initializenow falls back to inlineName-Realmcomposition wheneverGetNormalizedPlayer()yields nil (e.g. called beforeUnitNameresolves), not only when the library is entirely absent. Location:DeltaSync.lua.DebugStatusroster line now reports actual readiness —ready/loaded (not ready)/not loadedviaIsReady(), instead of merely whether the library is present. Location:DeltaSync.lua.
Packaging
LibGuildRoster-1.0is a required standalone-addon dependency, not a vendored copy..pkgmetaaddslibguildrostertorequired-dependencies(CurseForge auto-installs the GuildRoster addon);DeltaSync.tocdeclares## Dependencies: Ace3, AceCommQueue-1.0, GuildRosterand no longer carries aLibs\…load line for the roster lib. No duplicate copy ships in either the full or-nolibbuild, andGuildRoster(CFlibguildroster) loads before DeltaSync at runtime.- Interface levels bumped for the Midnight patches:
120001 → 120007and120000 → 120005. - Dependency version: the installed GuildRoster must be MINOR 6+ (adds
GetNormalizedPlayerand the sister-roster API). DeltaSync feature-detects each method, so an older GuildRoster degrades gracefully rather than erroring — but the cleaner paths stay inactive until it catches up. Publish GuildRoster MINOR 6+ on CurseForge before/with this DeltaSync release.
Migration Notes for Consumers
- Any consumer resolving
LibStub("GuildCache-1.0")must migrate toLibStub("LibGuildRoster-1.0")and apply two method renames:IsPlayerOnline→IsOnline,GetOnlineGuildMembers→GetOnlineMembers. Unchanged:NormalizeName,GetNormalizedPlayer,GetMember, and theOnMemberOnline/OnRosterReady(and other) CallbackHandler callbacks. ⚠️IsInGuildkeeps its name but is now a strict membership check — guard withIsReady()if you relied on the old accept-all-on-empty-roster behavior. - TOGProfessionMaster must migrate in lockstep. It depends on the standalone DeltaSync addon and resolves
GuildCache-1.0inTOGProfessionMaster.luaandScanner.lua; it will lose guild sync the moment it loads DeltaSync v3.0.0 until migrated. Do not publish DeltaSync v3.0.0 to CurseForge ahead of the matching TOGProfessionMaster release. - TOGBankClassic and other non-roster consumers — affected only if they resolved
GuildCache-1.0. Consumers using onlyDeltaOperations/ the P2P path with their ownisValidPeerneed no change.
LibStub MINOR
- Bumped from 10 → 11.
[v2.0.4] (2026-04-30) - SerializeBaseline carries type and parent end-to-end (MINOR 10)
Bug Fixes
SerializeBaseline/DeserializeBaselinewere silently dropping every field outside{hash, version, keys}— TOGProfessionMaster v0.2.0 introduced a hash-then-fetch sync protocol that encodes the request shape on the QUERY baseline as either{type = "leaf-data", keys = {...}}(fetch specific leaves) or{type = "subhashes", parent = "guild:cooldowns" | "guild:accountchars"}(drill into a roll-up). The lib's serializer hard-coded the payload table to three fields, so receivers'onDataRequestsawbaseline.type == nil, matched neither branch, and silently no-op'd. Every drill-down session in the requesting client stayed inACTIVEuntilDELIVERY_TIMEOUT(180s) fired — guild sync silently failed end-to-end since TOGPM v0.2.0; the only data flowing was spontaneous broadcasts triggered by local scans. Diagnosed live by adding ENTRY debug prints to TOGPM'sScanner.luaonDataRequestcallback, which showedtype=nil parent=nil keys=Non QUERY arrival, confirming the baseline had been gutted on the wire. Location: DeltaSync.lua:1223-1249.
Wire Format / Protocol
- Forward-compatible. MINOR<10 senders simply don't include
type/parent(existing behavior); MINOR>=10 receivers see them asniland behave as before. MINOR>=10 senders include the new fields; MINOR<10 receivers silently drop them (existing behavior). Mixed-version guilds will half-work — only the direction where the receiver is MINOR>=10 will honor the new request shapes — which is acceptable since TOGProfessionMaster v0.2.6 gates onMINOR >= 10and disables guild sync below that.
Migration Notes for Consumers
- TOGProfessionMaster v0.2.0+ — Required this fix. v0.2.6 ships gated on
LibStub("DeltaSync-1.0").MINOR >= 10and printsGuild sync disabledif it loads against an older DeltaSync. Don't ship TOGPM v0.2.6 to CurseForge until DeltaSync v2.0.4 is the published release on CurseForge. - TOGBankClassic and other MINOR<10-era consumers — No change required. They never set
baseline.type/baseline.parent, so the wire payload is bit-identical to MINOR 9 for them. - Detecting MINOR 10 — Consumers that need the new baseline-field carriage can check
LibStub("DeltaSync-1.0").MINOR >= 10(the value at theLibStub:NewLibrarycall site).
LibStub MINOR
- Bumped from 9 → 10.
[v2.0.3] (2026-04-29) - Hash-mismatch offer condition for content-aware-merge consumers (MINOR 9)
Protocol Changes
- Hash-mismatch offer condition —
P2PSession:OnHashListReceivednow offers data whenever its hash differs from the peer's, regardless ofupdatedAt. Previously a relayer with a slightly higherupdatedAt(which gets bumped on everyRebuildAll, even when content didn't change) would suppress legitimate offers from the actual data owner.updatedAtremains in the wire format for backwards compat but is no longer load-bearing for correctness. Consumers must merge content-aware on receive (max-wins for monotonic data, union for sets) for this to converge. Required by TOGProfessionMaster v0.2.0's relay-capable cooldown/recipe sync. Location: P2PSession.lua:241-258. - Candidate sort downgraded to heuristic — The descending-
updatedAtinsertion sort inP2PSession:OnOfferis 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. Comment in code documents the change. Location: P2PSession.lua:284-297. lib:SendHashOfferdoc-comment updated — Removed stale "only sends items where our updatedAt is strictly greater than the peer's" wording; replaced with the new caller contract. The function itself is unchanged — it serializes whatever the caller passes. Location: DeltaSync.lua:1149-1159.
Wire Format / Protocol
- Bit-identical to MINOR 8. The OFFER hash-list-broadcast and hash-offer messages still carry
{itemKey → {hash, updatedAt}}entries, the QUERY/RESPONSE/DATA channels are untouched, and the checksum envelope is unchanged. Old MINOR 7/8 consumers can still receive offers from a MINOR 9 sender — they'll just see more of them, and their existing offer-collection logic continues to pick the entry with the highestupdatedAt.
Migration Notes for Consumers
- Existing consumers (TOGBankClassic, etc.) — No change required. TOGBankClassic uses
DeltaOperationsbut not the P2P offer path, so this MINOR is a no-op for it. Any consumer that does use P2P will simply emit (and observe) more hash-offers when content actually differs. - New content-aware-merge consumers (TOGProfessionMaster v0.2.0+) — Must implement merge-on-receive semantics in
onDataReceived: max-wins for monotonic fields like cooldown remaining-time, union for sets like crafter lists. Without content-aware merge, two peers with concurrent edits will overwrite each other's changes. - Detecting MINOR 9 — Consumers that need to assert the new offer semantics can check
LibStub("DeltaSync-1.0").MINOR >= 9(the value at theLibStub:NewLibrarycall site).
LibStub MINOR
- Bumped from 8 → 9.
[v2.0.2] (2026-04-28) - GuildCache-1.0 callbacks + real-time online tracking + login-race retry (GuildCache MINOR 2)
New Features
- GuildCache-1.0 callbacks (CallbackHandler-1.0) — Consumers can now react to roster changes without polling. Six events fire via
lib.callbacks:Fire(...):OnRosterReady(once after the first successful non-empty rebuild),OnRosterUpdated(every rebuild),OnMemberOnline(name),OnMemberOffline(name),OnMemberJoined(name),OnMemberLeft(name). All payloads are canonical"Name-Realm"strings. Registration follows the standard CBH pattern:GuildCache.RegisterCallback(self, "OnMemberOnline", function(_, name) ... end)— first handler arg is the event name (CBH convention), second is the payload. Thelib.callbacks = lib.callbacks or CBH:New(lib)guard preserves consumer subscriptions across LibStub upgrades. Location: Libs/GuildCache-1.0/GuildCache-1.0.lua:50. - Real-time
CHAT_MSG_SYSTEMparsing for online/offline/join/leave transitions — Previously, online/offline transitions were only detected when WoW happened to fire a roster update. GuildCache now subscribes toCHAT_MSG_SYSTEMand matches the four guild system messages (has come online,has gone offline,has joined the guild,has left the guild/has been kicked out of the guild). The first three updatelib.guildRosterin place and fire the corresponding callback immediately; "joined" defers to the nextRequestGuildRoster()rebuild so the new entry has populated class/level/rank fields before consumers seeOnMemberJoined. TheGUILD_ROSTER_UPDATErebuild diff still fires the same callbacks as a catch-up safety net for any system message a consumer didn't see. Location: Libs/GuildCache-1.0/GuildCache-1.0.lua:300. lib:GetMember(name)query — Returns the full member entry ({name, class, level, rank, rankIndex, rankName, isOnline}) or nil. Useful for consumers that need the rank-index forisValidPeerfilters. Location: Libs/GuildCache-1.0/GuildCache-1.0.lua:287.- Member entry shape extended additively — Entries now include
name,rankIndex, andrankNamealongside the existingclass,level,rank,isOnlinefields. The legacyrankfield is retained as an alias forrankName(set to the same value) so MINOR=1 consumers that readentry.rankare unaffected.rankIndexunlocks rank-based peer filtering, which the rank-name string can't reliably do across guilds with custom rank labels.
Bug Fixes
- Login-race retry in
_RebuildGuildRoster—GetNumGuildMembers()returns 0 in a brief window afterPLAYER_LOGINeven when the player is in a guild. The previous rebuild silently produced an empty roster in that window and waited for the nextGUILD_ROSTER_UPDATEto recover, which could leave consumers' first roster query returning misleading results. The rebuild now detects this case (IsInGuild()true &&GetNumGuildMembers() == 0), incrementslib.retryCount, re-issuesRequestGuildRoster(), and bails. AfterMAX_RETRIES = 5consecutive zero-member responses it accepts the empty roster as the truth (player genuinely has no guild). Counter resets to 0 on the first successful non-empty rebuild or on a confirmed not-in-guild state. Location: Libs/GuildCache-1.0/GuildCache-1.0.lua:154-160. PLAYER_LOGINnow requests a roster instead of synchronously rebuilding — Old behavior:PLAYER_LOGINcalled_RebuildGuildRoster()directly, which under the new retry scheme would have eaten retries on the very first attempt. New behavior:PLAYER_LOGINonly invalidates the cached normalized player name and (ifIsInGuild()) callsRequestGuildRoster(). TheGUILD_ROSTER_UPDATEevent handler is the single canonical entry point for rebuilds, which keeps retry semantics clean. Location: Libs/GuildCache-1.0/GuildCache-1.0.lua:361-366.- Event re-registration on LibStub upgrade — Old behavior used
if not lib._guildCacheFrame then create end, which meant a hot upgrade from MINOR=1 to a higher MINOR that subscribes to a new event (e.g.CHAT_MSG_SYSTEM) would never receive that event — the existing frame kept its MINOR=1 subscription set. New behavior callsUnregisterAllEvents()then re-registers all needed events on every load, while reusing the same frame instance (so any consumer holding a reference is unaffected). Location: Libs/GuildCache-1.0/GuildCache-1.0.lua:352-358.
Improvements
- Diff-loop fires both online AND offline transitions (intentional deviation from the
LibGuildRoster-1.0reference implementation, which only fires online). Reasoning: a consumer registeringOnMemberOfflineshouldn't have an asymmetric gap depending on whetherCHAT_MSG_SYSTEMwas visible.CHAT_MSG_SYSTEMis still the low-latency primary driver — the diff is the catch-up safety net, and it now works both directions. Comment in code documents the divergence. Location: Libs/GuildCache-1.0/GuildCache-1.0.lua:200-217. wipe(lib.guildRoster)instead of table reassignment — Consumers that stashedlocal roster = GuildCache.guildRosterget the same updated table after a rebuild instead of holding a stale reference. Required for the new diff semantics to work correctly across calls..luarc.json— addedC_GuildInfoandGuildRostertodiagnostics.globalsso the lua-language-server stops flagging the new(C_GuildInfo and C_GuildInfo.GuildRoster) or GuildRostercompatibility shim.
API Surface for Migrating Consumers
- TOGProfessionMaster currently uses
LibStub("LibGuildRoster-1.0"):RegisterCallback("OnMemberOnline", fn)for crafter-online alerts and embedslibs/LibGuildRoster-1.0/. After this lands, TOGPM can swap toLibStub("GuildCache-1.0"):RegisterCallback("OnMemberOnline", fn)and drop the embedded folder — same callback name, same payload contract, samefunction(_, name)handler signature.
Wire Format / Protocol
- Unchanged. GuildCache is a pure local-roster library — no AceComm, no SavedVariables.
[v2.0.1] (2026-04-27) - Ship the missing DeltaSyncChannel.lua (MINOR 8)
Bug Fixes
DeltaSyncChannel.luawas missing from the repo, breaking every embedder that setchannelModule.enabled = true— v2.0.0 added the integration scaffolding for an optional CHANNEL transport (config parsing intoself.channelModule, dispatch fromSendMessagetoself:SendViaChannel, compact-codec branches inBroadcastData/OnComm_DATA), and a defensive error at DeltaSync.lua:497-502 that raises"channelModule.enabled = true but DeltaSyncChannel.lua was not loaded"when the implementation file is absent. The actual file — defininglib:InitChannelModule()(CHAT_MSG_CHANNEL frame setup, prefix-dispatch, self-filter, control-char strip) andlib:SendViaChannel(prefix, body, target)(SendChatMessage transport with 255-byte cap warning) — never got copied across from the consumer addon where it was authored. v2.0.1 ships the file. Embedders whoseInitialize({channelModule = {enabled = true, ...}})was hitting the defensive error in v2.0.0 will work after upgrading to MINOR 8. Location: new fileDeltaSyncChannel.lua, registered inDeltaSync.tocafterDeltaSync.lua.
New Files
DeltaSyncChannel.lua— optional CHANNEL transport, ~160 LOC. Adds two methods to theDeltaSync-1.0LibStub handle:InitChannelModule()(called automatically byInitializewhenchannelModule.enabled = true) andSendViaChannel(prefix, body, target)(called automatically bySendMessagefor any prefix whosechannelConfig.distribution == "CHANNEL"). Embedders that only use GUILD/PARTY/RAID/WHISPER do not need to load this file. WoW Classic silently dropsCHAT_MSG_ADDONfor custom channel numbers, so this module is the documented workaround: send via rawSendChatMessage(255-byte cap → host must supply a compactserializer/deserializerpair), receive via a dedicatedCHAT_MSG_CHANNELevent frame that dispatches into the existingOnComm_*handlers.
Improvements
- MINOR bumped 7 → 8 at DeltaSync.lua:9 — embedders whose private copies were on MINOR 7 (i.e. consumer addons that had been carrying their own
DeltaSyncChannel.luato work around the missing file) will now be overridden by the packaged MINOR 8 lib via LibStub, and can drop their embedded copies. PS's Libs/DeltaSync/DeltaSyncChannel.lua is the canonical source — same content, modulo aDeltaSync_Channel.lua→DeltaSyncChannel.luafilename harmonization in the header comment and missing-lib error string. .luarc.json— addedSendChatMessageandDEFAULT_CHAT_FRAMEtodiagnostics.globalsso the lua-language-server stops flagging the new transport's WoW API calls.
[v2.0.0] (2026-04-20) - DeltaSync-1.0 mod 7 Parity with TOGProfessionMaster
New Features
DeltaSync-1.0bumped toMINOR = 7, pulling the merged TOGPM (mod 2) / PersonalShopper (mod 6) superset into the standalone repo — The two forks that had been living in consumer addons at the sameDeltaSync-1.0MAJOR are now consolidated here. Every embedder whose private copy is at mod 2, mod 6, or mod 7 will now be overridden by this packaged version (LibStub picks the highest MINOR, so both forks and future embedders converge on the CurseForge-released lib). Location:DeltaSync.lua:8.NormalizeSenderpublic method — Newlib:NormalizeSender(name)canonicalizes AceComm's inconsistent sender strings (bare"Name"on same-realm vs"Name-Realm"on cross-realm). EveryOnComm_*handler now routes thesenderarg through it before comparison or dispatch, so self-ignore and peer lookups work identically regardless of realm topology. Location:DeltaSync.lua:800.onOfferReceived(sender, data)callback onInitializeconfig — New raw-OFFER inspection hook for consumers that want to observe hash-list-broadcasts without hooking into the full P2P session state machine. Fires before the P2PSession collect window processes the offer. Location:DeltaSync.lua:418.snifferFramefallback for non-AceComm receive paths — When a consumer has a comm prefix configured withdistribution = "CHANNEL"(raw public chat channel), AceComm's:RegisterCommdoesn't fire. DeltaSync now creates a dedicatedCHAT_MSG_ADDONevent frame that dispatches to the normalOnComm_*handlers, keeping CHANNEL-distributed messages on the same receive path as GUILD/WHISPER. Location:DeltaSync.lua:570.DebugStatusslash-style command — Newlib:DebugStatus()prints a diagnostic block: namespace, MINOR, player name, registered prefixes, and AceComm wiring state. Useful when an embedder's sends are silently dropping and you need to confirm which piece of the config is missing. Location:DeltaSync.lua:1412.- Debug category/tag toggle API — New
IsCategoryEnabled/SetCategoryEnabled/IsTagEnabled/SetTagEnabledplusGetDebugFrame/CreateDebugTab/RemoveDebugTab/BufferDebugMessage/RedrawDebugMessages. Gives host addons a structured way to surface DeltaSync's debug output in their own UI (the TOGPM debug tab consumes these). Location:DeltaSync.lua:1471-1658. GetPrefixInfo/IsPrefixAvailable/GetPeerStates/GetCommStatsintrospection API — Read-only accessors for the current prefix allocation, peer sync state table, and per-channel send/receive counts. Intended for debug panels and telemetry. Location:DeltaSync.lua:1760-1800.GetProtocolVersionreturns the wire-format revision — Returns the MINOR value, for embedders that want to guard calls behind a minimum version. Location:DeltaSync.lua:1406.
Breaking Changes
aceAddonis now a requiredInitialize()config key — Mod 7 no longer embedsAceComm-3.0intolib; instead it callsself.aceAddon:SendCommMessage(...)on the host addon's own AceAddon instance. An embedder'sInitialize({...})call that omitsaceAddonwill register receive handlers (via the new CHAT_MSG_ADDON sniffer fallback) but silently fail to send, putting them in a half-broken state. Migration: addaceAddon = self(or whatever holds yourAceAddon:NewAddon(...)handle) to the config table passed toInitialize. Location:DeltaSync.lua:451,DeltaSync.lua:753.AceSerializerno longer embedded intolib—lib:Serialize(...)andlib:Deserialize(...)methods thatAceSerializer:Embed(lib)would have added are gone. The library now callsLibStub("AceSerializer-3.0"):Serialize(...)at use-time (cached in a file-local upvalue) so it stays decoupled from Ace's MINOR upgrades and doesn't duplicate methods the host addon already has. Migration: consumers that calledlib:Serializemust switch toLibStub("AceSerializer-3.0"):Serializeon their own handle. Location:DeltaSync.lua:19-25.AceCommQueue-1.0throttling responsibility moved to host addon — Because mod 7 routes sends throughself.aceAddon:SendCommMessage, the wrap target for CRC-protection queuing is the host addon, not the library. Embedders mustLibStub("AceCommQueue-1.0"):Embed(theirAceAddon)themselves immediately afterNewAddon(...), or they'll see chunk-interleaving CRC corruption under sync load.## Dependencies: AceCommQueue-1.0remains inDeltaSync.tocso load order is still enforced. Migration: one-line:Embedcall in the consumer. Location: host-addon responsibility; documented inCLAUDE.md.- Wire format is unchanged but receive paths now normalize senders —
OnComm_*handlers route thesenderargument throughNormalizeSenderbefore comparison/dispatch. Messages from older-mod embedders still decode via the legacy AceSerializer-only fallback inDeserializeWithChecksum, so no user-visible wire break. Mentioned here because "same wire format, different handler behavior" is the kind of thing that bites during migration debugging. GuildCacheextracted into its own LibStub libraryGuildCache-1.0— Previously, roster-cache methods (NormalizeName,GetNormalizedPlayer,IsPlayerOnline,IsInGuild,GetOnlineGuildMembers) hung off theDeltaSync-1.0lib handle andguildRosterwas atlib.guildRoster. These have moved to a new LibStub library with MAJORGuildCache-1.0, embedded atLibs/GuildCache-1.0/GuildCache-1.0.lua. Consumers that calledDeltaSync:NormalizeName(x)(etc.) must now callLibStub("GuildCache-1.0"):NormalizeName(x), or grab the handle once:local GC = LibStub("GuildCache-1.0"); GC:NormalizeName(x). Motivation: GuildCache is independently useful (other addons can embed it without pulling the full sync stack) and follows the same "embedded initially → external dependency later" path DeltaSync itself is on. Same pattern TOGPM uses forLibGuildRoster-1.0. Inside DeltaSync, all consumers now route through a file-local upvalue with soft-dep presence checks, so embedders that drop GuildCache get graceful degradation (inline realm derivation, no whisper online-guard, accept-allisValidPeerdefault).
Improvements
- Cross-realm
NormalizeNamecorrectness — Previous implementation's regex"^(.-)%-(.+)$"matched greedy across every hyphen in the string, which broke for realm names containing hyphens (rare but possible). Replaced withtrimmed:match("%-[^%-]+$")— only appends the local realm for bare same-server names, and leaves any existing-Realmsuffix untouched so cross-realm peers are stored per-realm correctly. Includes an explicit doc note about AceComm's inconsistent sender-realm-suffix behavior. Location:GuildCache.lua:37-61. ComputeDelta/ApplyDeltaconvenience wrappers retained — Despite the substantial DeltaSync.lua rewrite, the wrapper methods that delegate toDeltaOperations.luaremain at the same call sites so consumer code built against v1.0.0 does not need to change delta-ops call sites. Location:DeltaSync.lua:1739-1748.DeltaOperations.luaandP2PSession.luaunchanged — Both files are byte-identical to the current TOGPM copy; no merge work required. All delta/hash/P2P session machinery works exactly as in v1.0.0.
[v1.0.0] (2026-04-03) - P2P Session Hardening, CRC Integrity & GuildCache Guard
New Features
DELIVERY_TIMEOUTfor in-flight data streams — After a peer repliessync-accept, the requester now arms aDELIVERY_TIMEOUTtimer (default 180s) so a provider that accepts and then goes silent can no longer pin an inbound session slot forever. On timeout the session transitions toFAILED, the slot frees, and catch-up logic retries another peer. Location:P2PSession.lua.TryAcquireSendSlot/ReleaseSendSlotoutbound accounting — Outbound sends now use explicit slot acquisition symmetric with the inboundmaxActiveSessionsmodel. A safety timer (SEND_TIMEOUT, default 90s) auto-releases slots the host forgets to release, so a crash in the host's send path can't deadlock the sender. Public API: host callslib.p2p:ReleaseSendSlot(requester)after the wire send returns. Location:P2PSession.lua.SerializeWithChecksum/DeserializeWithChecksumwrapped around all 7 channels — Every comm path (VERSION, DATA, QUERY, RESPONSE, DELTA, OFFER, HANDSHAKE) now goes through the checksum envelope (<AceSer payload>\030<checksum>\031END). Corrupt or truncated messages are detected at the edge and dropped with an INTEGRITY-MISMATCH log line instead of feeding garbage into delta apply. Graceful legacy-format fallback keeps compatibility with addons still on the older AceSerializer-only wire. Location:DeltaSync.lua.
Bug Fixes
- OFFER/HANDSHAKE session state machine gaps — Several sequences where a peer's
hash-offerarrived after the collect window closed, or async-acceptarrived for an item the dispatch loop had already given to another peer, left orphan session state. State transitions now validate against the current session table so late messages are ignored rather than mutating unrelated sessions. Location:P2PSession.lua. - Whispering offline guild members —
lib:SendMessage()with a WHISPER distribution would happily queue messages to members who had logged out, wasting the AceComm slot and causing spurious "no such player" errors in chat. Added a guard viaGuildCache:IsPlayerOnline(target)that drops the send with a debug log when the target isn't online. Location:DeltaSync.lua.
Improvements
docs/COMMUNICATION.mdbrought in line with the 7-channel reality — Documentation still described the original 5-channel VERSION/DATA/QUERY/RESPONSE/DELTA layout. Rewrote the channel table to include OFFER and HANDSHAKE, documented the hash-list-broadcast wire format and the sync-request/sync-accept/sync-busy handshake payloads, and fixed every markdownlint warning in the file. Location:docs/COMMUNICATION.md.docs/DESIGN.mdupdated for Phase 2 completion — Marked Phase 2 (P2P session layer) as complete, rewrote the architecture diagrams to show OFFER/HANDSHAKE as separate channels from DELTA, and added the "embedded vs global shared addon" section explaining the 16-prefix budget trade-off. All markdownlint errors fixed. Location:docs/DESIGN.md.- CurseForge description refreshed — Added the v0.0.3-alpha entry to the external changelog block consumed by CurseForge's project page. Location:
docs/Curseforge_Description.html.
[v0.0.3-alpha] (2026-04-02) - Standalone GuildCache Module
New Features
GuildCache.lua— generic guild presence cache — New file providingNormalizeName,GetNormalizedPlayer,IsPlayerOnline,IsInGuild, andGetOnlineGuildMembersoff thelibhandle. Maintainslib.guildRoster(normalizedName → {isOnline, class, level, rank}) viaGUILD_ROSTER_UPDATE. Extracted so consuming addons don't each have to reinvent name normalization and roster tracking, and so the library itself can gate whispers on online state. Location:GuildCache.lua.isValidPeercallback onInitP2P— Host addons can now filter which guildmates are eligible P2P peers (e.g. rank, role, "only trust officers"). Called during offer collection and dispatch; peers that fail the check are skipped. Location:P2PSession.lua.TableContains,TableCount,ValidateDeltautility methods onlib— Small helpers previously duplicated across consumers, now exposed centrally.ValidateDeltadoes version/timestamp/hash structural checks beforeApplyStructuredDeltaruns. Location:DeltaSync.lua,DeltaOperations.lua.
Bug Fixes
- QUERY channel was still using bare AceSerializer format — Every other channel had migrated to the checksum-wrapped format, but QUERY was missed, so queries crossing the wire were not integrity-checked. Migrated to
SerializeWithChecksum/DeserializeWithChecksumfor consistency and to catch the same class of truncation bugs. Location:DeltaSync.lua. - Extra
endsyntax error inDeltaSync.lua— A strayendkept the file from loading cleanly on fresh embed. Removed. Location:DeltaSync.lua. - Duplicated utility block in
DeltaSync.lua— A block of helper functions was copy-pasted during the earlier P2P merge; the duplicate shadowed the authoritative copy. Removed. Location:DeltaSync.lua. Initialize()doc comment referenced removed parameters — Cleaned up the header block so it matches the currentconfigschema. Location:DeltaSync.lua.
Improvements
Initialize()now derives local player identity via GuildCache — Instead of each host addon passingplayerName/playerFullNamethrough config,Initialize()callsGuildCache:GetNormalizedPlayer()on first use. Hosts that still pass these values override the derived ones. Location:DeltaSync.lua.P2PSessionnormalizes senders throughNorm()/Me()helpers —OnHashListReceivedandOnOfferused to compare sender strings directly, which broke when AceComm delivered bare names vs. fully-qualified names on the same payload. All sender comparisons now go through normalization. Location:P2PSession.lua..luarc.json— WoW API globals added — Several API functions (C_Timer,GetRealmName, etc.) were missing from the LSP globals list, causing noise in the problems panel. Added them. Location:.luarc.json.- Single-consumer constraint documented — Added a header block to
P2PSession.luaclarifying that one LibStub instance serves one embedding addon (the 7-prefix budget is per-addon, not per-library). Location:P2PSession.lua. - CurseForge description — multi-consumer claim corrected — Earlier marketing text implied two addons on the same client could share one DeltaSync instance. They can't (each gets its own LibStub copy with its own prefixes); wording updated. Location:
docs/Curseforge_Description.html.
[v0.0.2-alpha] (2026-04-01) - P2P Session Protocol & Ace3 Integration
New Features
P2PSession.lua— generalized port of TOGBankClassic's P2P protocol — New file implementing the broadcast / collect / dispatch / handshake loop:lib:BroadcastItemHasheskicks off a GUILD hash-list, peers reply withhash-offerwhispers during the collect window, dispatch picks the newest peer per stale item and sends async-request, and the peer repliessync-accept(initiates data exchange) orsync-busy(try next peer). No single "banker" bottleneck — any peer with fresh data can serve. Host integrates via callbacks onInitP2P(config):getMyHashes,hasContent,hasMissingItems,onSyncAccepted,onDataRequest,onDataReceived. Location:P2PSession.lua.- OFFER and HANDSHAKE channel types — Two new addon-message prefixes (
-oand-h) take the library from 5 channels to 7 out of the 16-prefix-per-addon WoW budget. OFFER carries hash-list-broadcasts (GUILD) and hash-offer replies (WHISPER); HANDSHAKE carries the three sync-request/accept/busy messages. Location:DeltaSync.lua. - High-level P2P API on
lib—BroadcastItemHashes(items, priority),SendHashOffer(target, items, priority),SendHandshake(target, payload, priority). These are thin wrappers overSendMessagebut document the protocol intent. Location:DeltaSync.lua. - Full CRC wire-format framework —
SerializeWithChecksum/DeserializeWithChecksumwrap every outgoing payload with an ASCII RS separator (\030), an additive checksum, and a stop marker (\031END). The deserializer does best-effort corrupt-payload decoding on INTEGRITY-MISMATCH so a debug log can still show what the peer was trying to send. Location:DeltaSync.lua.
Improvements
- Serialization switched from custom built-in to AceSerializer-3.0 — The earlier length-prefixed
SerializeData/DeserializeData(added in v0.0.1-alpha) was functional but every consuming addon already embedded AceSerializer anyway. Switched toAceSerialization-3.0via LibStub and embed it alongside AceComm/AceCommQueue inRegisterCommChannels, eliminating ~150 lines of hand-rolled serializer code. Location:DeltaSync.lua. AceCommQueue-1.0andAce3declared as hard dependencies —DeltaSync.tocnow lists them under## Dependencies, enforcing load order. Previous bundled copies underLibs/removed since AceCommQueue is shipped as a standalone addon. Location:DeltaSync.toc..pkgmetatightened for CurseForge release — Addedrequired-dependencies(initiallyace3,AceCommQueue-1.0), removed the now-unusedexternalsblock, and expanded theignorelist to catchdocs/**,*.ps1,*.bat, and the code-workspace file so they don't end up in the zip. Location:.pkgmeta.
Bug Fixes
AceCommQueue-1.0listed as a CurseForge required-dependency caused a 400/1018 upload error — CurseForge only accepts slugs for projects published on its platform; AceCommQueue is GitHub-only, so declaring it inrequired-dependencies:broke uploads. Removed from.pkgmetarequired-dependencies — load order is still enforced via## Dependencies:inDeltaSync.toc, which is all WoW actually cares about. Location:.pkgmeta.
[v0.0.1-alpha] (2026-03-17) - Initial Library Extraction from TOGBankClassic
New Features
- Library skeleton as
DeltaSync-1.0via LibStub — First pass at the standalone library, extracted and generalized from TOGBankClassic's proven delta sync system. Target: any WoW addon that needs guild-scoped P2P data sync can embed this library and get 90-99% bandwidth reduction on updates. Location:DeltaSync.lua,DeltaSync.toc. - Built-in length-prefixed serializer —
SerializeData/DeserializeDatareplace the earliertostring()placeholder with a real type-tagged format supporting tables, strings, numbers, booleans, nil, and circular-reference protection. (Superseded in v0.0.2-alpha by AceSerializer, but the API surface remains so host addons don't have to change call sites.) Location:DeltaSync.lua. ComputeDelta()/ApplyDelta()convenience wrappers onlib— Shorthand that delegates toComputeStructuredDelta/ApplyStructuredDeltainDeltaOperations.luaso hosts don't have to know the module split. Location:DeltaSync.lua.- Self-ignore on all 5 message handlers — VERSION, QUERY, RESPONSE, DATA, and DELTA now drop messages whose sender matches the local player. Handled for both bare
UnitNameand fully-qualifiedName-Realmformats because AceComm delivers either depending on realm context. Without this, every broadcast you sent came back and triggered your own handlers. Location:DeltaSync.lua.
Bug Fixes
- Duplicate
ValidateDelta()inDeltaSync.lua— A stub version shadowed the complete implementation inDeltaOperations.lua(which does full version/timestamp/hash validation). Removed the stub so the authoritative one is always used. Location:DeltaSync.lua.
Improvements
playerNameandplayerFullNamestored duringInitialize()— Cached onlibat init time so handlers don't re-query the WoW API on every incoming message. Location:DeltaSync.lua.
This mod has no additional files