Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/tests/logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,18 @@
expect((records[0]?.context?.nested as StructuredLogMeta).authToken).toBe("[redacted]");
});

it("cleanup function removes the transport so subsequent logs are not delivered", () => {
const records: StructuredLogRecord[] = [];
const unregister = registerLogTransport({ write: (record) => records.push(record) });
const logger = createLogger({ logLevel: "info" });

logger.info("before.unregister");
unregister();
logger.info("after.unregister");

expect(records.map((r) => r.message)).toEqual(["before.unregister"]);

Check failure on line 156 in src/tests/logger.test.ts

View workflow job for this annotation

GitHub Actions / Test

src/tests/logger.test.ts > shared/logger > createLogger > cleanup function removes the transport so subsequent logs are not delivered

AssertionError: expected [ 'before.unregister', …(1) ] to deeply equal [ 'before.unregister' ] - Expected + Received Array [ "before.unregister", + "after.unregister", ] ❯ src/tests/logger.test.ts:156:45
});

it("supports per-module log levels and custom transports", () => {
const records: StructuredLogRecord[] = [];
const logger = createLogger({
Expand Down
48 changes: 48 additions & 0 deletions src/tests/walletStatusTracker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,52 @@ describe("wallet status tracker", () => {
expect(getAdapterName(WalletType.FREIGHTER)).toBe("Freighter");
expect(truncatePublicKey("short")).toBe("short");
});

describe("batchUpdates", () => {
it("coalesces rapid state changes into one listener call per microtask flush", async () => {
const listener = vi.fn();
const tracker = new WalletStatusTracker({ batchUpdates: true });
tracker.subscribe(listener);

tracker.setError("first");
tracker.setDisconnected();
tracker.setError("last");

// No calls yet — flush is deferred to microtask
expect(listener).not.toHaveBeenCalled();

await Promise.resolve();

// Exactly one call, with the latest state
expect(listener).toHaveBeenCalledTimes(1);
expect(listener.mock.calls[0]![0].status).toBe("error");
expect(listener.mock.calls[0]![0].error).toBe("last");
});

it("without batchUpdates, each state change calls listeners synchronously", () => {
const listener = vi.fn();
const tracker = new WalletStatusTracker();
tracker.subscribe(listener);

tracker.setError("a");
tracker.setDisconnected();
tracker.setError("b");

expect(listener).toHaveBeenCalledTimes(3);
});

it("does not emit after destroy even when a flush was pending", async () => {
const listener = vi.fn();
const tracker = new WalletStatusTracker({ batchUpdates: true });
tracker.subscribe(listener);

tracker.setError("will be destroyed");
tracker.destroy();

await Promise.resolve();

// destroy cleared _flushPending so the queued microtask finds no listeners
expect(listener).not.toHaveBeenCalled();
});
});
});
23 changes: 23 additions & 0 deletions src/wallet/walletStatusTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export type WalletStatusUnsubscribe = () => void;

export interface WalletStatusTrackerConfig {
onStatusChange?: WalletStatusListener;
/**
* When true, rapid status changes within the same microtask checkpoint are
* coalesced and emitted once. Listeners always receive the latest state.
* Defaults to false (synchronous, unchanged behaviour).
*/
batchUpdates?: boolean;
}

const ADAPTER_NAMES: Record<WalletType, string> = {
Expand Down Expand Up @@ -96,11 +102,14 @@ const INITIAL_STATUS: WalletStatus = {
export class WalletStatusTracker {
private _status: WalletStatus = { ...INITIAL_STATUS };
private _listeners: Set<WalletStatusListener> = new Set();
private _batchUpdates: boolean;
private _flushPending = false;

constructor(config?: WalletStatusTrackerConfig) {
if (config?.onStatusChange) {
this._listeners.add(config.onStatusChange);
}
this._batchUpdates = config?.batchUpdates ?? false;
}

get status(): WalletStatus {
Expand Down Expand Up @@ -131,6 +140,19 @@ export class WalletStatusTracker {
}

private _emit(): void {
if (this._batchUpdates) {
if (!this._flushPending) {
this._flushPending = true;
queueMicrotask(() => {
this._flushPending = false;
const status = this._status;
for (const listener of this._listeners) {
listener(status);
}
});
}
return;
}
const status = this._status;
for (const listener of this._listeners) {
listener(status);
Expand Down Expand Up @@ -244,5 +266,6 @@ export class WalletStatusTracker {
destroy(): void {
this._listeners.clear();
this._status = { ...INITIAL_STATUS };
this._flushPending = false;
}
}
Loading