Stop notifications after removing a host (#3176)
* fix(notifications): stop pushes after host removal Push tokens were persisted indefinitely with no relationship to app pairing state. Give registrations a 48-hour renewable lease so offline unpairing converges, and revoke immediately when the host is reachable. * fix(notifications): make token revocation bounded and durable
This commit is contained in:
@@ -967,6 +967,59 @@ test("keeps the transport connected when a session RPC ping times out", async ()
|
||||
expect(client.getConnectionState().status).toBe("connected");
|
||||
});
|
||||
|
||||
test("waits for the daemon to acknowledge push token revocation", async () => {
|
||||
const mock = createMockTransport();
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_push_revocation",
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen({ features: { pushTokenRevocation: true } });
|
||||
await connectPromise;
|
||||
|
||||
const revocation = client.unregisterPushToken("ExponentPushToken[test-device]");
|
||||
const request = parseSentFrame(mock.sent.at(-1));
|
||||
expect(request).toMatchObject({
|
||||
type: "push.unregister.request",
|
||||
token: "ExponentPushToken[test-device]",
|
||||
});
|
||||
mock.triggerMessage(
|
||||
wrapSessionMessage({
|
||||
type: "push.unregister.response",
|
||||
payload: { requestId: request.requestId },
|
||||
}),
|
||||
);
|
||||
|
||||
await revocation;
|
||||
});
|
||||
|
||||
test("bounds the wait for push token revocation", async () => {
|
||||
useHeartbeatClock();
|
||||
const mock = createMockTransport();
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "clsk_push_revocation_timeout",
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
const connectPromise = client.connect();
|
||||
mock.triggerOpen({ features: { pushTokenRevocation: true } });
|
||||
await connectPromise;
|
||||
|
||||
const revocation = client.unregisterPushToken("ExponentPushToken[test-device]");
|
||||
const rejection = expect(revocation).rejects.toThrow("Timeout waiting for message (2000ms)");
|
||||
await vi.advanceTimersByTimeAsync(1_999);
|
||||
expect(client.getConnectionState().status).toBe("connected");
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await rejection;
|
||||
expect(client.getConnectionState().status).toBe("connected");
|
||||
});
|
||||
|
||||
test("defaults session RPC waiters to sixty seconds", async () => {
|
||||
useHeartbeatClock();
|
||||
const logger = createMockLogger();
|
||||
|
||||
@@ -876,6 +876,7 @@ function toTimeoutError(error: unknown, label: string, timeoutMs: number): Error
|
||||
const DEFAULT_RECONNECT_BASE_DELAY_MS = 1500;
|
||||
const DEFAULT_RECONNECT_MAX_DELAY_MS = 30000;
|
||||
const DEFAULT_SESSION_RPC_TIMEOUT_MS = 60_000;
|
||||
const PUSH_TOKEN_REVOCATION_TIMEOUT_MS = 2_000;
|
||||
const DEFAULT_CONNECT_TIMEOUT_MS = 15_000;
|
||||
const DEFAULT_LIVENESS_TIMEOUT_MS = 5000;
|
||||
const LIVENESS_HEARTBEAT_INTERVAL_MS = 10_000;
|
||||
@@ -1840,6 +1841,16 @@ export class DaemonClient {
|
||||
});
|
||||
}
|
||||
|
||||
async unregisterPushToken(token: string): Promise<void> {
|
||||
const requestId = this.createRequestId();
|
||||
await this.sendCorrelatedSessionRequest({
|
||||
requestId,
|
||||
message: { type: "push.unregister.request", token, requestId },
|
||||
responseType: "push.unregister.response",
|
||||
timeout: PUSH_TOKEN_REVOCATION_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
async ping(params?: { requestId?: string; timeoutMs?: number }): Promise<{
|
||||
requestId: string;
|
||||
clientSentAt: number;
|
||||
|
||||
Reference in New Issue
Block a user