Make ConsentManager referentially stable and add subscribe method

getAll() now returns a cached snapshot that is only replaced on state
changes, making it safe for useSyncExternalStore. The new subscribe()
method unifies onReady + onChange with immediate replay so React
consumers get a single subscription point.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-05-13 18:04:10 +04:00
committed by Émile Ré
parent 4078567c04
commit c17757565f

View File

@@ -18,7 +18,7 @@ type Callback = (consent: ConsentData) => void;
export class ConsentManager { export class ConsentManager {
private _ready = false; private _ready = false;
private _hasResponse = false; private _hasResponse = false;
private _consent: ConsentData = {}; private _snapshot: ConsentData = {};
private readonly _readyListeners: Callback[] = []; private readonly _readyListeners: Callback[] = [];
private readonly _changeListeners: Callback[] = []; private readonly _changeListeners: Callback[] = [];
@@ -31,16 +31,25 @@ export class ConsentManager {
} }
has(category: string): boolean { has(category: string): boolean {
return !!this._consent[category]; return !!this._snapshot[category];
} }
getAll(): ConsentData { getAll(): ConsentData {
return { ...this._consent }; return this._snapshot;
}
subscribe(cb: Callback): () => void {
if (this._ready) {
cb(this._snapshot);
}
const offReady = this.onReady(cb);
const offChange = this.onChange(cb);
return () => { offReady(); offChange(); };
} }
onReady(cb: Callback): () => void { onReady(cb: Callback): () => void {
if (this._ready) { if (this._ready) {
cb({ ...this._consent }); cb(this._snapshot);
return () => {}; return () => {};
} }
this._readyListeners.push(cb); this._readyListeners.push(cb);
@@ -60,23 +69,23 @@ export class ConsentManager {
/** @internal Called by CookieBannerClient when consent state is first resolved. */ /** @internal Called by CookieBannerClient when consent state is first resolved. */
_setReady(consent: ConsentData, hasResponse: boolean): void { _setReady(consent: ConsentData, hasResponse: boolean): void {
this._consent = consent; this._snapshot = { ...consent };
this._hasResponse = hasResponse; this._hasResponse = hasResponse;
this._ready = true; this._ready = true;
for (const cb of this._readyListeners.splice(0)) { for (const cb of this._readyListeners.splice(0)) {
cb({ ...this._consent }); cb(this._snapshot);
} }
for (const cb of this._changeListeners) { for (const cb of this._changeListeners) {
cb({ ...this._consent }); cb(this._snapshot);
} }
} }
/** @internal Called by CookieBannerClient when consent changes after user action. */ /** @internal Called by CookieBannerClient when consent changes after user action. */
_notify(consent: ConsentData): void { _notify(consent: ConsentData): void {
this._consent = consent; this._snapshot = { ...consent };
this._hasResponse = true; this._hasResponse = true;
for (const cb of this._changeListeners) { for (const cb of this._changeListeners) {
cb({ ...this._consent }); cb(this._snapshot);
} }
} }
} }