From e135d37878ffa7c4c9249384662022cc5548a07b Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:41:48 +0100 Subject: [PATCH 1/3] Change SubscriptionFilters to extend Set --- packages/lib/src/subscriptions.ts | 42 ++++++++++++++++--------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/lib/src/subscriptions.ts b/packages/lib/src/subscriptions.ts index aeb08827..f5ac8f61 100644 --- a/packages/lib/src/subscriptions.ts +++ b/packages/lib/src/subscriptions.ts @@ -14,25 +14,27 @@ export type EventSubscriberCallback = (event: T | null, synced: boolean) => v * When isEmpty() is true, no strings match this filter. * Otherwise, literal string matching is performed against a set. */ -export class SubscriptionFilters { +export class SubscriptionFilters extends Set { private constructor( private _all: boolean, - private _filters: Set, - ) {} + _filters: Iterable = [], + ) { + super(_filters); + } static jsonSchema = Type.Array(Type.String()); static fromJSON(json: Static) { - return new this(false, new Set(json)); + return new this(false, json); } toJSON() { - return [...this._filters]; + return [...this]; } /** Creates a subscription filter accepting all strings */ static all() { - return new this(true, new Set()); + return new this(true); } isAll() { @@ -41,11 +43,11 @@ export class SubscriptionFilters { /** Creates a subscription filter rejecting all strings */ static empty() { - return new this(false, new Set()); + return new this(false); } isEmpty() { - return !this._all && this._filters.size === 0; + return !this._all && this.size === 0; } /** @@ -67,20 +69,20 @@ export class SubscriptionFilters { if (this._all) { return "[SubscriptionFilters All]"; } - if (this._filters.size === 0) { + if (this.size === 0) { return "[SubscriptionFilters Empty]"; } - return `[SubscriptionFilters Set<${this._filters.size}>]`; + return `[SubscriptionFilters Set<${this.size}>]`; } /** Returns true if this filter accepts the given string */ accepts(value: string) { - return this._all || this._filters.has(value); + return this._all || this.has(value); } /** Returns true if this filter extends the other */ extends(other: this) { - return this._all || (!other._all && [...other._filters.values()].every(value => this._filters.has(value))); + return this._all || (!other._all && [...other.values()].every(value => this.has(value))); } /** Returns true if there is overlap between two filters */ @@ -91,8 +93,8 @@ export class SubscriptionFilters { if (other._all) { return !this.isEmpty(); } - for (const filter of other._filters) { - if (this._filters.has(filter)) { + for (const filter of other) { + if (this.has(filter)) { return true; } } @@ -106,11 +108,11 @@ export class SubscriptionFilters { } if (other._all) { this._all = true; - this._filters.clear(); + this.clear(); return; } - for (const filter of other._filters) { - this._filters.add(filter); + for (const filter of other) { + this.add(filter); } } @@ -122,14 +124,14 @@ export class SubscriptionFilters { subtract(other: this) { if (other._all) { this._all = false; - this._filters.clear(); + this.clear(); return; } if (this._all) { return; // Cannot subtract from all } - for (const filter of other._filters) { - this._filters.delete(filter); + for (const filter of other) { + this.delete(filter); } } } From 95ea073b77833c128c0cadea9bb569d39d4857a2 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Fri, 26 Jun 2026 22:04:06 +0100 Subject: [PATCH 2/3] Expose error on event subscriber snapshot --- packages/lib/src/subscriptions.ts | 20 +++++++++++++++----- test/lib/subscriptions.test.js | 22 +++++++++++++++++++++- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/lib/src/subscriptions.ts b/packages/lib/src/subscriptions.ts index f5ac8f61..72b2b24b 100644 --- a/packages/lib/src/subscriptions.ts +++ b/packages/lib/src/subscriptions.ts @@ -1,6 +1,6 @@ import { Type, Static } from "@sinclair/typebox"; import { Link, Event, EventClass, RequestHandler, WebSocketBaseConnector } from "./link"; -import { Address, MessageRequest, IUser, JsonBoolean, StringEnum } from "./data"; +import { Address, MessageRequest, IUser, JsonBoolean, StringEnum, AccountDetails } from "./data"; import isDeepStrictEqual from "./is_deep_strict_equal"; import { RequestError } from "./errors"; import { logger } from "./logging"; @@ -408,8 +408,10 @@ export class EventSubscriber { lastUpdatedMs = -1; /** True if this subscriber is currently synced with the source */ synced = false; + /** Error value from last attempted subscription event */ + error = null as Error | null; /** Repeat calls to getSnapshot will return the same readonly copy unless values has updated */ - private _snapshot: readonly [S, boolean] = [this.makeSnapshot(), false]; + private _snapshot: readonly [S, boolean, Error | null] = [this.makeSnapshot(), false, null]; private _snapshotLastUpdatedMs = -1; /** Callbacks will be called when an event is received or the synced state changes */ private _callbacks = new Array>(); @@ -557,10 +559,14 @@ export class EventSubscriber { * Obtain a snapshot of the current state of the tracked resource * @returns tuple of values map snapshot and synced property. */ - getSnapshot(): readonly [S, boolean] { - if (this._snapshotLastUpdatedMs !== this.lastUpdatedMs || this._snapshot[1] !== this.synced) { + getSnapshot(): readonly [S, boolean, Error | null] { + if ( + this._snapshotLastUpdatedMs !== this.lastUpdatedMs + || this._snapshot[1] !== this.synced + || this._snapshot[2] !== this.error + ) { this._snapshotLastUpdatedMs = this.lastUpdatedMs; - this._snapshot = [this.makeSnapshot(), this.synced]; + this._snapshot = [this.makeSnapshot(), this.synced, this.error]; } return this._snapshot; } @@ -583,6 +589,7 @@ export class EventSubscriber { const updatesSent = await this.control.sendTo("controller", new SubscriptionRequest( entry.name, action, this.lastUpdatedMs, filters )); + this.error = null; if (!updatesSent) { this.synced = this._hasSubscriptions(); this._notify(null); @@ -591,6 +598,9 @@ export class EventSubscriber { if (!(err instanceof RequestError) || err.code !== "SessionLost") { logger.error(`Unexpected error updating ${entry.name} subscription:\n${err.stack}`); } + if (err instanceof Error) { + this.error = err; + } } } } diff --git a/test/lib/subscriptions.test.js b/test/lib/subscriptions.test.js index 93f330b7..3425f60b 100644 --- a/test/lib/subscriptions.test.js +++ b/test/lib/subscriptions.test.js @@ -792,7 +792,10 @@ describe("lib/subscriptions", function() { }); describe("class EventSubscriber", function() { - let mockControl, eventSubscriber; + /** @type {MockControl} */ + let mockControl; + /** @type {lib.EventSubscriber} */ + let eventSubscriber; beforeEach(function() { mockControl = new MockControl(new MockConnector( @@ -1120,6 +1123,23 @@ describe("lib/subscriptions", function() { } assert.equal(logged, undefined); }); + + it("should expose errors through the snapshot", async function() { + eventSubscriber.control.sendTo = function() { + throw new lib.PermissionError("Permission denied"); + }; + + const unsubscribe = eventSubscriber.subscribe(() => {}); + await new Promise(r => setImmediate(r)); + const [,, error1] = eventSubscriber.getSnapshot(); + assert.deepEqual(error1, new lib.PermissionError("Permission denied")); + + eventSubscriber.control.sendTo = function() {}; + unsubscribe(); + await new Promise(r => setImmediate(r)); + const [,, error2] = eventSubscriber.getSnapshot(); + assert.deepEqual(error2, null); + }); }); }); From 7e89e7fb6fca38bb9f8b59fc0470e310fe0aed50 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Fri, 26 Jun 2026 22:15:28 +0100 Subject: [PATCH 3/3] Fix assert in test case --- test/lib/subscriptions.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/lib/subscriptions.test.js b/test/lib/subscriptions.test.js index 3425f60b..313b2968 100644 --- a/test/lib/subscriptions.test.js +++ b/test/lib/subscriptions.test.js @@ -1132,7 +1132,9 @@ describe("lib/subscriptions", function() { const unsubscribe = eventSubscriber.subscribe(() => {}); await new Promise(r => setImmediate(r)); const [,, error1] = eventSubscriber.getSnapshot(); - assert.deepEqual(error1, new lib.PermissionError("Permission denied")); + assert.ok(error1 instanceof lib.PermissionError); + assert.equal(error1.message, "Permission denied"); + assert.equal(error1.code, "PermissionError"); eventSubscriber.control.sendTo = function() {}; unsubscribe();