1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-12-13 07:42:14 +03:00

Delayed event management: split endpoints, no auth (#5066)

* Delayed event management: split endpoints, no auth

Add dedicated endpoints for each of the cancel/restart/send actions for
updating a delayed event, and make them unauthenticated.

Also keep support for the original endpoint where the update action is
in the request body, and make the split-endpoint versions fall back to
it if they are unsupported by the homeserver.

* Don't @link parameters in method docstrings

as TypeDoc doesn't support that

* Reduce code duplication

* Reduce code duplication again

* Add a little more test coverage

* Use split delayed event management for widgets

* Specify which eslint rule to ignore

Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>

* Restore embedded non-split delay evt update method

Keep supporting it to not break widgets that currently use it.
Also add back the test for it.

* Deprecate the non-split delay evt update methods

* Comment to explain fallback to non-split endpoint

* Add backwards compatibility with authed endpoints

* Comment backwards compatibility helper method

* Await returned promises

because `return await promise` is at least as fast as `return promise`

---------

Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>
This commit is contained in:
Andrew Ferrazzutti
2025-11-11 00:54:33 -05:00
committed by GitHub
parent 1dee1ba581
commit df88edfda0
12 changed files with 484 additions and 82 deletions

View File

@@ -56,7 +56,7 @@
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"loglevel": "^1.9.2", "loglevel": "^1.9.2",
"matrix-events-sdk": "0.0.1", "matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.10.0", "matrix-widget-api": "^1.14.0",
"oidc-client-ts": "^3.0.1", "oidc-client-ts": "^3.0.1",
"p-retry": "7", "p-retry": "7",
"sdp-transform": "^3.0.0", "sdp-transform": "^3.0.0",

View File

@@ -86,7 +86,9 @@ class MockWidgetApi extends EventEmitter {
? { event_id: `$${Math.random()}` } ? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` }, : { delay_id: `id-${Math.random()}` },
); );
public updateDelayedEvent = jest.fn().mockResolvedValue(undefined); public cancelScheduledDelayedEvent = jest.fn().mockResolvedValue(undefined);
public restartScheduledDelayedEvent = jest.fn().mockResolvedValue(undefined);
public sendScheduledDelayedEvent = jest.fn().mockResolvedValue(undefined);
public sendToDevice = jest.fn().mockResolvedValue(undefined); public sendToDevice = jest.fn().mockResolvedValue(undefined);
public requestOpenIDConnectToken = jest.fn(async () => { public requestOpenIDConnectToken = jest.fn(async () => {
return testOIDCToken; return testOIDCToken;
@@ -531,17 +533,49 @@ describe("RoomWidgetClient", () => {
).rejects.toThrow(); ).rejects.toThrow();
}); });
it("updates delayed events", async () => { it.each([UpdateDelayedEventAction.Cancel, UpdateDelayedEventAction.Restart, UpdateDelayedEventAction.Send])(
"can %s scheduled delayed events (action in parameter)",
async (action: UpdateDelayedEventAction) => {
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(
MatrixCapabilities.MSC4157UpdateDelayedEvent,
);
await client._unstable_updateDelayedEvent("id", action);
let updateDelayedEvent: (delayId: string) => Promise<unknown>;
switch (action) {
case UpdateDelayedEventAction.Cancel:
updateDelayedEvent = widgetApi.cancelScheduledDelayedEvent;
break;
case UpdateDelayedEventAction.Restart:
updateDelayedEvent = widgetApi.cancelScheduledDelayedEvent;
break;
case UpdateDelayedEventAction.Send:
updateDelayedEvent = widgetApi.sendScheduledDelayedEvent;
break;
}
expect(updateDelayedEvent).toHaveBeenCalledWith("id");
},
);
it("can cancel scheduled delayed events (action in method)", async () => {
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] }); await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent); expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
for (const action of [ await client._unstable_cancelScheduledDelayedEvent("id");
UpdateDelayedEventAction.Cancel, expect(widgetApi.cancelScheduledDelayedEvent).toHaveBeenCalledWith("id");
UpdateDelayedEventAction.Restart, });
UpdateDelayedEventAction.Send,
]) { it("can restart scheduled delayed events (action in method)", async () => {
await client._unstable_updateDelayedEvent("id", action); await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith("id", action); expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
} await client._unstable_restartScheduledDelayedEvent("id");
expect(widgetApi.restartScheduledDelayedEvent).toHaveBeenCalledWith("id");
});
it("can send scheduled delayed events (action in method)", async () => {
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
await client._unstable_sendScheduledDelayedEvent("id");
expect(widgetApi.sendScheduledDelayedEvent).toHaveBeenCalledWith("id");
}); });
}); });
@@ -583,6 +617,13 @@ describe("RoomWidgetClient", () => {
"Server does not support", "Server does not support",
); );
} }
for (const updateDelayedEvent of [
client._unstable_cancelScheduledDelayedEvent,
client._unstable_restartScheduledDelayedEvent,
client._unstable_sendScheduledDelayedEvent,
]) {
await expect(updateDelayedEvent.call(client, "id")).rejects.toThrow("Server does not support");
}
}); });
}); });
}); });

View File

@@ -801,6 +801,10 @@ describe("MatrixClient", function () {
await expect( await expect(
client._unstable_updateDelayedEvent("anyDelayId", UpdateDelayedEventAction.Send), client._unstable_updateDelayedEvent("anyDelayId", UpdateDelayedEventAction.Send),
).rejects.toThrow(errorMessage); ).rejects.toThrow(errorMessage);
await expect(client._unstable_cancelScheduledDelayedEvent("anyDelayId")).rejects.toThrow(errorMessage);
await expect(client._unstable_restartScheduledDelayedEvent("anyDelayId")).rejects.toThrow(errorMessage);
await expect(client._unstable_sendScheduledDelayedEvent("anyDelayId")).rejects.toThrow(errorMessage);
}); });
it("works with null threadId", async () => { it("works with null threadId", async () => {
@@ -1077,21 +1081,169 @@ describe("MatrixClient", function () {
}); });
}); });
it("can update delayed events", async () => { it.each([UpdateDelayedEventAction.Cancel, UpdateDelayedEventAction.Restart, UpdateDelayedEventAction.Send])(
"can %s scheduled delayed events (action in request body)",
async (action: UpdateDelayedEventAction) => {
const delayId = "id";
httpLookups = [
{
method: "POST",
prefix: unstableMSC4140Prefix,
path: `/delayed_events/${encodeURIComponent(delayId)}`,
data: {
action,
},
},
];
await client._unstable_updateDelayedEvent(delayId, action);
},
);
it.each([UpdateDelayedEventAction.Cancel, UpdateDelayedEventAction.Restart, UpdateDelayedEventAction.Send])(
"can %s scheduled delayed events (action in request body fallback when auth required)",
async (action: UpdateDelayedEventAction) => {
const delayId = "id";
const baseLookup = {
method: "POST",
prefix: unstableMSC4140Prefix,
path: `/delayed_events/${encodeURIComponent(delayId)}`,
};
httpLookups = [
{
...baseLookup,
error: {
httpStatus: 401,
errcode: "M_MISSING_TOKEN",
},
},
{
...baseLookup,
data: {
action,
},
},
];
await client._unstable_updateDelayedEvent(delayId, action);
},
);
it("can cancel scheduled delayed events (action in request path)", async () => {
const delayId = "id"; const delayId = "id";
const action = UpdateDelayedEventAction.Restart;
httpLookups = [ httpLookups = [
{
method: "POST",
prefix: unstableMSC4140Prefix,
path: `/delayed_events/${encodeURIComponent(delayId)}/cancel`,
},
];
await client._unstable_cancelScheduledDelayedEvent(delayId);
});
it("can restart scheduled delayed events (action in request path)", async () => {
const delayId = "id";
httpLookups = [
{
method: "POST",
prefix: unstableMSC4140Prefix,
path: `/delayed_events/${encodeURIComponent(delayId)}/restart`,
},
];
await client._unstable_restartScheduledDelayedEvent(delayId);
});
it("can send scheduled delayed events (action in request path)", async () => {
const delayId = "id";
httpLookups = [
{
method: "POST",
prefix: unstableMSC4140Prefix,
path: `/delayed_events/${encodeURIComponent(delayId)}/send`,
},
];
await client._unstable_sendScheduledDelayedEvent(delayId);
});
it("can cancel scheduled delayed events (action in request path fallback when unsupported)", async () => {
const delayId = "id";
httpLookups = [
{
method: "POST",
prefix: unstableMSC4140Prefix,
path: `/delayed_events/${encodeURIComponent(delayId)}/cancel`,
error: {
httpStatus: 400,
errcode: "M_UNRECOGNIZED",
},
},
{ {
method: "POST", method: "POST",
prefix: unstableMSC4140Prefix, prefix: unstableMSC4140Prefix,
path: `/delayed_events/${encodeURIComponent(delayId)}`, path: `/delayed_events/${encodeURIComponent(delayId)}`,
data: { data: {
action, action: UpdateDelayedEventAction.Cancel,
}, },
}, },
]; ];
await client._unstable_updateDelayedEvent(delayId, action); await client._unstable_cancelScheduledDelayedEvent(delayId);
expect(httpLookups).toHaveLength(0);
});
it("can restart scheduled delayed events (action in request path fallback when unsupported)", async () => {
const delayId = "id";
httpLookups = [
{
method: "POST",
prefix: unstableMSC4140Prefix,
path: `/delayed_events/${encodeURIComponent(delayId)}/restart`,
error: {
httpStatus: 400,
errcode: "M_UNRECOGNIZED",
},
},
{
method: "POST",
prefix: unstableMSC4140Prefix,
path: `/delayed_events/${encodeURIComponent(delayId)}`,
data: {
action: UpdateDelayedEventAction.Restart,
},
},
];
await client._unstable_restartScheduledDelayedEvent(delayId);
expect(httpLookups).toHaveLength(0);
});
it("can send scheduled delayed events (action in request path fallback when unsupported)", async () => {
const delayId = "id";
httpLookups = [
{
method: "POST",
prefix: unstableMSC4140Prefix,
path: `/delayed_events/${encodeURIComponent(delayId)}/send`,
error: {
httpStatus: 400,
errcode: "M_UNRECOGNIZED",
},
},
{
method: "POST",
prefix: unstableMSC4140Prefix,
path: `/delayed_events/${encodeURIComponent(delayId)}`,
data: {
action: UpdateDelayedEventAction.Send,
},
},
];
await client._unstable_sendScheduledDelayedEvent(delayId);
expect(httpLookups).toHaveLength(0);
}); });
}); });

View File

@@ -523,6 +523,9 @@ describe("MatrixRTCSession", () => {
client.sendEvent = sendEventMock; client.sendEvent = sendEventMock;
client._unstable_updateDelayedEvent = jest.fn(); client._unstable_updateDelayedEvent = jest.fn();
client._unstable_cancelScheduledDelayedEvent = jest.fn();
client._unstable_restartScheduledDelayedEvent = jest.fn();
client._unstable_sendScheduledDelayedEvent = jest.fn();
mockRoom = makeMockRoom([]); mockRoom = makeMockRoom([]);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession); sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);

View File

@@ -94,6 +94,9 @@ describe("MembershipManager", () => {
// Provide a default mock that is like the default "non error" server behaviour. // Provide a default mock that is like the default "non error" server behaviour.
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" }); (client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined); (client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
(client._unstable_cancelScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
(client._unstable_sendScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
(client._unstable_sendStickyEvent as Mock<any>).mockResolvedValue({ event_id: "id" }); (client._unstable_sendStickyEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
(client._unstable_sendStickyDelayedEvent as Mock<any>).mockResolvedValue({ delay_id: "id" }); (client._unstable_sendStickyDelayedEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
(client.sendStateEvent as Mock<any>).mockResolvedValue({ event_id: "id" }); (client.sendStateEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
@@ -122,7 +125,9 @@ describe("MembershipManager", () => {
it("sends a membership event and schedules delayed leave when joining a call", async () => { it("sends a membership event and schedules delayed leave when joining a call", async () => {
// Spys/Mocks // Spys/Mocks
const updateDelayedEventHandle = createAsyncHandle<void>(client._unstable_updateDelayedEvent as Mock); const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
client._unstable_restartScheduledDelayedEvent as Mock,
);
// Test // Test
const memberManager = new MembershipManager(undefined, room, client, callSession); const memberManager = new MembershipManager(undefined, room, client, callSession);
@@ -143,7 +148,7 @@ describe("MembershipManager", () => {
}, },
"_@alice:example.org_AAAAAAA_m.call", "_@alice:example.org_AAAAAAA_m.call",
); );
updateDelayedEventHandle.resolve?.(); restartScheduledDelayedEventHandle.resolve?.();
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith( expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
room.roomId, room.roomId,
{ delay: 8000 }, { delay: 8000 },
@@ -157,13 +162,13 @@ describe("MembershipManager", () => {
it("reschedules delayed leave event if sending state cancels it", async () => { it("reschedules delayed leave event if sending state cancels it", async () => {
const memberManager = new MembershipManager(undefined, room, client, callSession); const memberManager = new MembershipManager(undefined, room, client, callSession);
const waitForSendState = waitForMockCall(client.sendStateEvent); const waitForSendState = waitForMockCall(client.sendStateEvent);
const waitForUpdateDelaye = waitForMockCallOnce( const waitForRestartScheduledDelayedEvent = waitForMockCallOnce(
client._unstable_updateDelayedEvent, client._unstable_restartScheduledDelayedEvent,
Promise.reject(new MatrixError({ errcode: "M_NOT_FOUND" })), Promise.reject(new MatrixError({ errcode: "M_NOT_FOUND" })),
); );
memberManager.join([focus], focusActive); memberManager.join([focus], focusActive);
await waitForSendState; await waitForSendState;
await waitForUpdateDelaye; await waitForRestartScheduledDelayedEvent;
await jest.advanceTimersByTimeAsync(1); await jest.advanceTimersByTimeAsync(1);
// Once for the initial event and once because of the errcode: "M_NOT_FOUND" // Once for the initial event and once because of the errcode: "M_NOT_FOUND"
// Different to "sends a membership event and schedules delayed leave when joining a call" where its only called once (1) // Different to "sends a membership event and schedules delayed leave when joining a call" where its only called once (1)
@@ -179,7 +184,7 @@ describe("MembershipManager", () => {
if (useOwnedStateEvents) { if (useOwnedStateEvents) {
room.getVersion = jest.fn().mockReturnValue("org.matrix.msc3757.default"); room.getVersion = jest.fn().mockReturnValue("org.matrix.msc3757.default");
} }
const updatedDelayedEvent = waitForMockCall(client._unstable_updateDelayedEvent); const restartScheduledDelayedEvent = waitForMockCall(client._unstable_restartScheduledDelayedEvent);
const sentDelayedState = waitForMockCall( const sentDelayedState = waitForMockCall(
client._unstable_sendDelayedStateEvent, client._unstable_sendDelayedStateEvent,
Promise.resolve({ Promise.resolve({
@@ -265,13 +270,13 @@ describe("MembershipManager", () => {
await sentDelayedState; await sentDelayedState;
// should have prepared the heartbeat to keep delaying the leave event while still connected // should have prepared the heartbeat to keep delaying the leave event while still connected
await updatedDelayedEvent; await restartScheduledDelayedEvent;
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1); expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
// ensures that we reach the code that schedules the timeout for the next delay update before we advance the timers. // ensures that we reach the code that schedules the timeout for the next delay update before we advance the timers.
await jest.advanceTimersByTimeAsync(5000); await jest.advanceTimersByTimeAsync(5000);
// should update delayed disconnect // should update delayed disconnect
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(2); expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(2);
} }
it("sends a membership event after rate limits during delayed event setup when joining a call", async () => { it("sends a membership event after rate limits during delayed event setup when joining a call", async () => {
@@ -343,7 +348,7 @@ describe("MembershipManager", () => {
// (onRTCSessionMemberUpdate) // (onRTCSessionMemberUpdate)
// - Only then do we resolve the sending of the delayed event. // - Only then do we resolve the sending of the delayed event.
// - We test that the manager acknowledges the leave and sends a new membership state event. // - We test that the manager acknowledges the leave and sends a new membership state event.
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValueOnce( (client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValueOnce(
new MatrixError({ errcode: "M_NOT_FOUND" }), new MatrixError({ errcode: "M_NOT_FOUND" }),
); );
@@ -404,17 +409,17 @@ describe("MembershipManager", () => {
manager.join([focus]); manager.join([focus]);
await jest.advanceTimersByTimeAsync(1); await jest.advanceTimersByTimeAsync(1);
await manager.leave(); await manager.leave();
expect(client._unstable_updateDelayedEvent).toHaveBeenLastCalledWith("id", "send"); expect(client._unstable_sendScheduledDelayedEvent).toHaveBeenLastCalledWith("id");
expect(client.sendStateEvent).toHaveBeenCalled(); expect(client.sendStateEvent).toHaveBeenCalled();
}); });
it("send leave event when leave is called and resolving delayed leave fails", async () => { it("send leave event when leave is called and resolving delayed leave fails", async () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus]); manager.join([focus]);
await jest.advanceTimersByTimeAsync(1); await jest.advanceTimersByTimeAsync(1);
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue("unknown"); (client._unstable_sendScheduledDelayedEvent as Mock<any>).mockRejectedValue("unknown");
await manager.leave(); await manager.leave();
// We send a normal leave event since we failed using updateDelayedEvent with the "send" action. // We send a normal leave event since we failed using sendScheduledDelayedEvent.
expect(client.sendStateEvent).toHaveBeenLastCalledWith( expect(client.sendStateEvent).toHaveBeenLastCalledWith(
room.roomId, room.roomId,
"org.matrix.msc3401.call.member", "org.matrix.msc3401.call.member",
@@ -438,6 +443,9 @@ describe("MembershipManager", () => {
expect(client.sendStateEvent).not.toHaveBeenCalled(); expect(client.sendStateEvent).not.toHaveBeenCalled();
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled(); expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled(); expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
expect(client._unstable_cancelScheduledDelayedEvent).not.toHaveBeenCalled();
expect(client._unstable_restartScheduledDelayedEvent).not.toHaveBeenCalled();
expect(client._unstable_sendScheduledDelayedEvent).not.toHaveBeenCalled();
}); });
it("does nothing if own membership still present", async () => { it("does nothing if own membership still present", async () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new MembershipManager({}, room, client, callSession);
@@ -447,6 +455,9 @@ describe("MembershipManager", () => {
// reset all mocks before checking what happens when calling: `onRTCSessionMemberUpdate` // reset all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
(client.sendStateEvent as Mock).mockClear(); (client.sendStateEvent as Mock).mockClear();
(client._unstable_updateDelayedEvent as Mock).mockClear(); (client._unstable_updateDelayedEvent as Mock).mockClear();
(client._unstable_cancelScheduledDelayedEvent as Mock).mockClear();
(client._unstable_restartScheduledDelayedEvent as Mock).mockClear();
(client._unstable_sendScheduledDelayedEvent as Mock).mockClear();
(client._unstable_sendDelayedStateEvent as Mock).mockClear(); (client._unstable_sendDelayedStateEvent as Mock).mockClear();
await manager.onRTCSessionMemberUpdate([ await manager.onRTCSessionMemberUpdate([
@@ -462,6 +473,9 @@ describe("MembershipManager", () => {
expect(client.sendStateEvent).not.toHaveBeenCalled(); expect(client.sendStateEvent).not.toHaveBeenCalled();
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled(); expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled(); expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
expect(client._unstable_cancelScheduledDelayedEvent).not.toHaveBeenCalled();
expect(client._unstable_restartScheduledDelayedEvent).not.toHaveBeenCalled();
expect(client._unstable_sendScheduledDelayedEvent).not.toHaveBeenCalled();
}); });
it("recreates membership if it is missing", async () => { it("recreates membership if it is missing", async () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new MembershipManager({}, room, client, callSession);
@@ -469,7 +483,7 @@ describe("MembershipManager", () => {
await jest.advanceTimersByTimeAsync(1); await jest.advanceTimersByTimeAsync(1);
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate` // clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
(client.sendStateEvent as Mock).mockClear(); (client.sendStateEvent as Mock).mockClear();
(client._unstable_updateDelayedEvent as Mock).mockClear(); (client._unstable_restartScheduledDelayedEvent as Mock).mockClear();
(client._unstable_sendDelayedStateEvent as Mock).mockClear(); (client._unstable_sendDelayedStateEvent as Mock).mockClear();
// Our own membership is removed: // Our own membership is removed:
@@ -478,7 +492,7 @@ describe("MembershipManager", () => {
expect(client.sendStateEvent).toHaveBeenCalled(); expect(client.sendStateEvent).toHaveBeenCalled();
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled(); expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled();
expect(client._unstable_updateDelayedEvent).toHaveBeenCalled(); expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalled();
}); });
it("updates the UpdateExpiry entry in the action scheduler", async () => { it("updates the UpdateExpiry entry in the action scheduler", async () => {
@@ -487,10 +501,10 @@ describe("MembershipManager", () => {
await jest.advanceTimersByTimeAsync(1); await jest.advanceTimersByTimeAsync(1);
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate` // clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
(client.sendStateEvent as Mock).mockClear(); (client.sendStateEvent as Mock).mockClear();
(client._unstable_updateDelayedEvent as Mock).mockClear(); (client._unstable_restartScheduledDelayedEvent as Mock).mockClear();
(client._unstable_sendDelayedStateEvent as Mock).mockClear(); (client._unstable_sendDelayedStateEvent as Mock).mockClear();
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValueOnce( (client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValueOnce(
new MatrixError({ errcode: "M_NOT_FOUND" }), new MatrixError({ errcode: "M_NOT_FOUND" }),
); );
@@ -503,7 +517,7 @@ describe("MembershipManager", () => {
expect(client.sendStateEvent).toHaveBeenCalled(); expect(client.sendStateEvent).toHaveBeenCalled();
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled(); expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled();
expect(client._unstable_updateDelayedEvent).toHaveBeenCalled(); expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalled();
expect(manager.status).toBe(Status.Connected); expect(manager.status).toBe(Status.Connected);
}); });
}); });
@@ -523,17 +537,17 @@ describe("MembershipManager", () => {
// The first call is from checking id the server deleted the delayed event // The first call is from checking id the server deleted the delayed event
// so it does not need a `advanceTimersByTime` // so it does not need a `advanceTimersByTime`
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1); expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
// TODO: Check that update delayed event is called with the correct HTTP request timeout // TODO: Check that update delayed event is called with the correct HTTP request timeout
// expect(client._unstable_updateDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 }); // expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 });
for (let i = 2; i <= 12; i++) { for (let i = 2; i <= 12; i++) {
// flush promises before advancing the timers to make sure schedulers are setup // flush promises before advancing the timers to make sure schedulers are setup
await jest.advanceTimersByTimeAsync(10_000); await jest.advanceTimersByTimeAsync(10_000);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(i); expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(i);
// TODO: Check that update delayed event is called with the correct HTTP request timeout // TODO: Check that update delayed event is called with the correct HTTP request timeout
// expect(client._unstable_updateDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 }); // expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 });
} }
}); });
@@ -681,7 +695,7 @@ describe("MembershipManager", () => {
}); });
describe("retries sending update delayed leave event restart", () => { describe("retries sending update delayed leave event restart", () => {
it("resends the initial check delayed update event", async () => { it("resends the initial check delayed update event", async () => {
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue( (client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValue(
new MatrixError( new MatrixError(
{ errcode: "M_LIMIT_EXCEEDED" }, { errcode: "M_LIMIT_EXCEEDED" },
429, 429,
@@ -695,17 +709,17 @@ describe("MembershipManager", () => {
// Hit rate limit // Hit rate limit
await jest.advanceTimersByTimeAsync(1); await jest.advanceTimersByTimeAsync(1);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1); expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
// Hit second rate limit. // Hit second rate limit.
await jest.advanceTimersByTimeAsync(1000); await jest.advanceTimersByTimeAsync(1000);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(2); expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(2);
// Setup resolve // Setup resolve
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined); (client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
await jest.advanceTimersByTimeAsync(1000); await jest.advanceTimersByTimeAsync(1000);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(3); expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(3);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1); expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
}); });
}); });
@@ -734,7 +748,7 @@ describe("MembershipManager", () => {
// because legacy does not have a retry limit and no mechanism to communicate unrecoverable errors. // because legacy does not have a retry limit and no mechanism to communicate unrecoverable errors.
it("throws, when reaching maximum number of retries", async () => { it("throws, when reaching maximum number of retries", async () => {
const delayEventRestartError = jest.fn(); const delayEventRestartError = jest.fn();
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue( (client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValue(
new MatrixError( new MatrixError(
{ errcode: "M_LIMIT_EXCEEDED" }, { errcode: "M_LIMIT_EXCEEDED" },
429, 429,
@@ -808,11 +822,11 @@ describe("MembershipManager", () => {
manager.join([focus], focusActive); manager.join([focus], focusActive);
try { try {
// Let the scheduler run one iteration so that we can send the join state event // Let the scheduler run one iteration so that we can send the join state event
await waitForMockCall(client._unstable_updateDelayedEvent); await waitForMockCall(client._unstable_restartScheduledDelayedEvent);
// We never resolve the delayed event so that we can test the probablyLeft event. // We never resolve the delayed event so that we can test the probablyLeft event.
// This simulates the case where the server does not respond to the delayed event. // This simulates the case where the server does not respond to the delayed event.
client._unstable_updateDelayedEvent = jest.fn(() => stuckPromise); client._unstable_restartScheduledDelayedEvent = jest.fn(() => stuckPromise);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1); expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
expect(manager.status).toBe(Status.Connected); expect(manager.status).toBe(Status.Connected);
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true); expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
@@ -822,18 +836,18 @@ describe("MembershipManager", () => {
await jest.advanceTimersByTimeAsync(5000); await jest.advanceTimersByTimeAsync(5000);
// No emission after 5s // No emission after 5s
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true); expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1); expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
await jest.advanceTimersByTimeAsync(4999); await jest.advanceTimersByTimeAsync(4999);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(3); expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(3);
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true); expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
// Reset mocks before we setup the next delayed event restart by advancing the timers 1 more ms. // Reset mocks before we setup the next delayed event restart by advancing the timers 1 more ms.
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue({}); (client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue({});
// Emit after 10s // Emit after 10s
await jest.advanceTimersByTimeAsync(1); await jest.advanceTimersByTimeAsync(1);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(4); expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(4);
expect(probablyLeftEmit).toHaveBeenCalledWith(true); expect(probablyLeftEmit).toHaveBeenCalledWith(true);
// Mock a sync which does not include our own membership // Mock a sync which does not include our own membership
@@ -898,8 +912,8 @@ describe("MembershipManager", () => {
describe("join()", () => { describe("join()", () => {
describe("sends an rtc membership event", () => { describe("sends an rtc membership event", () => {
it("sends a membership event and schedules delayed leave when joining a call", async () => { it("sends a membership event and schedules delayed leave when joining a call", async () => {
const updateDelayedEventHandle = createAsyncHandle<void>( const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
client._unstable_updateDelayedEvent as Mock, client._unstable_restartScheduledDelayedEvent as Mock,
); );
const memberManager = new StickyEventMembershipManager(undefined, room, client, callSession); const memberManager = new StickyEventMembershipManager(undefined, room, client, callSession);
@@ -925,7 +939,7 @@ describe("MembershipManager", () => {
msc4354_sticky_key: "_@alice:example.org_AAAAAAA_m.call", msc4354_sticky_key: "_@alice:example.org_AAAAAAA_m.call",
}, },
); );
updateDelayedEventHandle.resolve?.(); restartScheduledDelayedEventHandle.resolve?.();
// Ensure we have sent the delayed disconnect event. // Ensure we have sent the delayed disconnect event.
expect(client._unstable_sendStickyDelayedEvent).toHaveBeenCalledWith( expect(client._unstable_sendStickyDelayedEvent).toHaveBeenCalledWith(

View File

@@ -52,6 +52,9 @@ export type MockClient = Pick<
| "sendStateEvent" | "sendStateEvent"
| "_unstable_sendDelayedStateEvent" | "_unstable_sendDelayedStateEvent"
| "_unstable_updateDelayedEvent" | "_unstable_updateDelayedEvent"
| "_unstable_cancelScheduledDelayedEvent"
| "_unstable_restartScheduledDelayedEvent"
| "_unstable_sendScheduledDelayedEvent"
| "_unstable_sendStickyEvent" | "_unstable_sendStickyEvent"
| "_unstable_sendStickyDelayedEvent" | "_unstable_sendStickyDelayedEvent"
| "cancelPendingEvent" | "cancelPendingEvent"
@@ -67,6 +70,9 @@ export function makeMockClient(userId: string, deviceId: string): MockClient {
sendStateEvent: jest.fn(), sendStateEvent: jest.fn(),
cancelPendingEvent: jest.fn(), cancelPendingEvent: jest.fn(),
_unstable_updateDelayedEvent: jest.fn(), _unstable_updateDelayedEvent: jest.fn(),
_unstable_cancelScheduledDelayedEvent: jest.fn(),
_unstable_restartScheduledDelayedEvent: jest.fn(),
_unstable_sendScheduledDelayedEvent: jest.fn(),
_unstable_sendDelayedStateEvent: jest.fn(), _unstable_sendDelayedStateEvent: jest.fn(),
_unstable_sendStickyEvent: jest.fn(), _unstable_sendStickyEvent: jest.fn(),
_unstable_sendStickyDelayedEvent: jest.fn(), _unstable_sendStickyDelayedEvent: jest.fn(),

View File

@@ -106,6 +106,7 @@ import { RoomMemberEvent, type RoomMemberEventHandlerMap } from "./models/room-m
import { type IPowerLevelsContent, type RoomStateEvent, type RoomStateEventHandlerMap } from "./models/room-state.ts"; import { type IPowerLevelsContent, type RoomStateEvent, type RoomStateEventHandlerMap } from "./models/room-state.ts";
import { import {
isSendDelayedEventRequestOpts, isSendDelayedEventRequestOpts,
UpdateDelayedEventAction,
type DelayedEventInfo, type DelayedEventInfo,
type IAddThreePidOnlyBody, type IAddThreePidOnlyBody,
type IBindThreePidBody, type IBindThreePidBody,
@@ -130,7 +131,6 @@ import {
type KnockRoomOpts, type KnockRoomOpts,
type SendDelayedEventRequestOpts, type SendDelayedEventRequestOpts,
type SendDelayedEventResponse, type SendDelayedEventResponse,
type UpdateDelayedEventAction,
} from "./@types/requests.ts"; } from "./@types/requests.ts";
import { import {
type AccountDataEvents, type AccountDataEvents,
@@ -3570,8 +3570,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* *
* Note: This endpoint is unstable, and can throw an `Error`. * Note: This endpoint is unstable, and can throw an `Error`.
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details. * Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
*
* @deprecated Instead use one of:
* - {@link _unstable_cancelScheduledDelayedEvent}
* - {@link _unstable_restartScheduledDelayedEvent}
* - {@link _unstable_sendScheduledDelayedEvent}
*/ */
// eslint-disable-next-line // eslint-disable-next-line @typescript-eslint/naming-convention
public async _unstable_updateDelayedEvent( public async _unstable_updateDelayedEvent(
delayId: string, delayId: string,
action: UpdateDelayedEventAction, action: UpdateDelayedEventAction,
@@ -3583,17 +3588,123 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
"updateDelayedEvent", "updateDelayedEvent",
); );
} }
return await this.updateScheduledDelayedEventWithActionInBody(delayId, action, requestOptions);
}
/**
* Cancel the scheduled delivery of the delayed event matching the provided delayId.
*
* Note: This endpoint is unstable, and can throw an `Error`.
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
*
* @throws A M_NOT_FOUND error if no matching delayed event could be found.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public async _unstable_cancelScheduledDelayedEvent(
delayId: string,
requestOptions: IRequestOpts = {},
): Promise<EmptyObject> {
return await this.updateScheduledDelayedEvent(delayId, UpdateDelayedEventAction.Cancel, requestOptions);
}
/**
* Restart the scheduled delivery of the delayed event matching the given delayId.
*
* Note: This endpoint is unstable, and can throw an `Error`.
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
*
* @throws A M_NOT_FOUND error if no matching delayed event could be found.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public async _unstable_restartScheduledDelayedEvent(
delayId: string,
requestOptions: IRequestOpts = {},
): Promise<EmptyObject> {
return await this.updateScheduledDelayedEvent(delayId, UpdateDelayedEventAction.Restart, requestOptions);
}
/**
* Immediately send the delayed event matching the given delayId,
* instead of waiting for its scheduled delivery.
*
* Note: This endpoint is unstable, and can throw an `Error`.
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
*
* @throws A M_NOT_FOUND error if no matching delayed event could be found.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public async _unstable_sendScheduledDelayedEvent(
delayId: string,
requestOptions: IRequestOpts = {},
): Promise<EmptyObject> {
return await this.updateScheduledDelayedEvent(delayId, UpdateDelayedEventAction.Send, requestOptions);
}
private async updateScheduledDelayedEvent(
delayId: string,
action: UpdateDelayedEventAction,
requestOptions: IRequestOpts = {},
): Promise<EmptyObject> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw new UnsupportedDelayedEventsEndpointError(
"Server does not support the delayed events API",
`${action}ScheduledDelayedEvent`,
);
}
try {
const path = utils.encodeUri("/delayed_events/$delayId/$action", {
$delayId: delayId,
$action: action,
});
return await this.http.request(Method.Post, path, undefined, undefined, {
...requestOptions,
prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_DELAYED_EVENTS}`,
});
} catch (e) {
if (e instanceof MatrixError && e.errcode === "M_UNRECOGNIZED") {
// For backwards compatibility with an older version of this endpoint
// which put the update action in the request body instead of the path
return await this.updateScheduledDelayedEventWithActionInBody(delayId, action, requestOptions);
} else {
throw e;
}
}
}
/**
* @deprecated Present for backwards compatibility with an older version of MSC4140
* which had a single, authenticated endpoint for updating a delayed event, instead
* of one unauthenticated endpoint per update action.
*/
private async updateScheduledDelayedEventWithActionInBody(
delayId: string,
action: UpdateDelayedEventAction,
requestOptions: IRequestOpts = {},
): Promise<EmptyObject> {
const path = utils.encodeUri("/delayed_events/$delayId", { const path = utils.encodeUri("/delayed_events/$delayId", {
$delayId: delayId, $delayId: delayId,
}); });
const data = { const data = {
action, action,
}; };
return await this.http.authedRequest(Method.Post, path, undefined, data, { try {
...requestOptions, return await this.http.request(Method.Post, path, undefined, data, {
prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_DELAYED_EVENTS}`, ...requestOptions,
}); prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_DELAYED_EVENTS}`,
});
} catch (e) {
if (e instanceof MatrixError && e.errcode === "M_MISSING_TOKEN") {
// For backwards compatibility with an older version of this endpoint
// which required authentication
return await this.http.authedRequest(Method.Post, path, undefined, data, {
...requestOptions,
prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_DELAYED_EVENTS}`,
});
} else {
throw e;
}
}
} }
/** /**

View File

@@ -37,7 +37,7 @@ import {
type ISendEventResponse, type ISendEventResponse,
type SendDelayedEventRequestOpts, type SendDelayedEventRequestOpts,
type SendDelayedEventResponse, type SendDelayedEventResponse,
type UpdateDelayedEventAction, UpdateDelayedEventAction,
} from "./@types/requests.ts"; } from "./@types/requests.ts";
import { EventType, type StateEvents } from "./@types/event.ts"; import { EventType, type StateEvents } from "./@types/event.ts";
import { logger } from "./logger.ts"; import { logger } from "./logger.ts";
@@ -459,8 +459,12 @@ export class RoomWidgetClient extends MatrixClient {
/** /**
* @experimental This currently relies on an unstable MSC (MSC4140). * @experimental This currently relies on an unstable MSC (MSC4140).
* @deprecated Instead use one of:
* - {@link _unstable_cancelScheduledDelayedEvent}
* - {@link _unstable_restartScheduledDelayedEvent}
* - {@link _unstable_sendScheduledDelayedEvent}
*/ */
// eslint-disable-next-line // eslint-disable-next-line @typescript-eslint/naming-convention
public async _unstable_updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise<EmptyObject> { public async _unstable_updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise<EmptyObject> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) { if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw new UnsupportedDelayedEventsEndpointError( throw new UnsupportedDelayedEventsEndpointError(
@@ -469,7 +473,67 @@ export class RoomWidgetClient extends MatrixClient {
); );
} }
await this.widgetApi.updateDelayedEvent(delayId, action).catch(timeoutToConnectionError); let updateDelayedEvent: (delayId: string) => Promise<unknown>;
switch (action) {
case UpdateDelayedEventAction.Cancel:
updateDelayedEvent = this.widgetApi.cancelScheduledDelayedEvent;
break;
case UpdateDelayedEventAction.Restart:
updateDelayedEvent = this.widgetApi.cancelScheduledDelayedEvent;
break;
case UpdateDelayedEventAction.Send:
updateDelayedEvent = this.widgetApi.sendScheduledDelayedEvent;
break;
}
await updateDelayedEvent.call(this.widgetApi, delayId).catch(timeoutToConnectionError);
return {};
}
/**
* @experimental This currently relies on an unstable MSC (MSC4140).
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public async _unstable_cancelScheduledDelayedEvent(delayId: string): Promise<EmptyObject> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw new UnsupportedDelayedEventsEndpointError(
"Server does not support the delayed events API",
"cancelScheduledDelayedEvent",
);
}
await this.widgetApi.cancelScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
return {};
}
/**
* @experimental This currently relies on an unstable MSC (MSC4140).
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public async _unstable_restartScheduledDelayedEvent(delayId: string): Promise<EmptyObject> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw new UnsupportedDelayedEventsEndpointError(
"Server does not support the delayed events API",
"restartScheduledDelayedEvent",
);
}
await this.widgetApi.restartScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
return {};
}
/**
* @experimental This currently relies on an unstable MSC (MSC4140).
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public async _unstable_sendScheduledDelayedEvent(delayId: string): Promise<EmptyObject> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw new UnsupportedDelayedEventsEndpointError(
"Server does not support the delayed events API",
"sendScheduledDelayedEvent",
);
}
await this.widgetApi.sendScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
return {}; return {};
} }

View File

@@ -59,7 +59,14 @@ export class ClientStoppedError extends Error {
export class UnsupportedDelayedEventsEndpointError extends Error { export class UnsupportedDelayedEventsEndpointError extends Error {
public constructor( public constructor(
message: string, message: string,
public clientEndpoint: "sendDelayedEvent" | "updateDelayedEvent" | "sendDelayedStateEvent" | "getDelayedEvents", public clientEndpoint:
| "sendDelayedEvent"
| "updateDelayedEvent"
| "cancelScheduledDelayedEvent"
| "restartScheduledDelayedEvent"
| "sendScheduledDelayedEvent"
| "sendDelayedStateEvent"
| "getDelayedEvents",
) { ) {
super(message); super(message);
this.name = "UnsupportedDelayedEventsEndpointError"; this.name = "UnsupportedDelayedEventsEndpointError";

View File

@@ -178,7 +178,8 @@ export interface MembershipConfig {
* In the presence of network packet loss (hurting TCP connections), the custom delayedEventRestartLocalTimeoutMs * In the presence of network packet loss (hurting TCP connections), the custom delayedEventRestartLocalTimeoutMs
* helps by keeping more delayed event reset candidates in flight, * helps by keeping more delayed event reset candidates in flight,
* improving the chances of a successful reset. (its is equivalent to the js-sdk `localTimeout` configuration, * improving the chances of a successful reset. (its is equivalent to the js-sdk `localTimeout` configuration,
* but only applies to calls to the `_unstable_updateDelayedEvent` endpoint with a body of `{action:"restart"}`.) * but only applies to calls to the `_unstable_restartScheduledDelayedEvent` endpoint
* or the `_unstable_updateDelayedEvent` endpoint with a body of `{action:"restart"}`.)
*/ */
delayedLeaveEventRestartLocalTimeoutMs?: number; delayedLeaveEventRestartLocalTimeoutMs?: number;
@@ -514,6 +515,9 @@ export class MatrixRTCSession extends TypedEventEmitter<
| "sendStateEvent" | "sendStateEvent"
| "_unstable_sendDelayedStateEvent" | "_unstable_sendDelayedStateEvent"
| "_unstable_updateDelayedEvent" | "_unstable_updateDelayedEvent"
| "_unstable_cancelScheduledDelayedEvent"
| "_unstable_restartScheduledDelayedEvent"
| "_unstable_sendScheduledDelayedEvent"
| "_unstable_sendStickyEvent" | "_unstable_sendStickyEvent"
| "_unstable_sendStickyDelayedEvent" | "_unstable_sendStickyDelayedEvent"
| "cancelPendingEvent" | "cancelPendingEvent"

View File

@@ -16,11 +16,7 @@ limitations under the License.
import { AbortError } from "p-retry"; import { AbortError } from "p-retry";
import { EventType, RelationType } from "../@types/event.ts"; import { EventType, RelationType } from "../@types/event.ts";
import { import { type ISendEventResponse, type SendDelayedEventResponse } from "../@types/requests.ts";
type ISendEventResponse,
type SendDelayedEventResponse,
UpdateDelayedEventAction,
} from "../@types/requests.ts";
import { type EmptyObject } from "../@types/common.ts"; import { type EmptyObject } from "../@types/common.ts";
import type { MatrixClient } from "../client.ts"; import type { MatrixClient } from "../client.ts";
import { ConnectionError, HTTPError, MatrixError } from "../http-api/errors.ts"; import { ConnectionError, HTTPError, MatrixError } from "../http-api/errors.ts";
@@ -169,7 +165,14 @@ function createReplaceActionUpdate(type: MembershipActionType, offset?: number):
type MembershipManagerClient = Pick< type MembershipManagerClient = Pick<
MatrixClient, MatrixClient,
"getUserId" | "getDeviceId" | "sendStateEvent" | "_unstable_sendDelayedStateEvent" | "_unstable_updateDelayedEvent" | "getUserId"
| "getDeviceId"
| "sendStateEvent"
| "_unstable_sendDelayedStateEvent"
| "_unstable_updateDelayedEvent"
| "_unstable_cancelScheduledDelayedEvent"
| "_unstable_restartScheduledDelayedEvent"
| "_unstable_sendScheduledDelayedEvent"
>; >;
/** /**
@@ -544,7 +547,7 @@ export class MembershipManager
private async cancelKnownDelayIdBeforeSendDelayedEvent(delayId: string): Promise<ActionUpdate> { private async cancelKnownDelayIdBeforeSendDelayedEvent(delayId: string): Promise<ActionUpdate> {
// Remove all running updates and restarts // Remove all running updates and restarts
return await this.client return await this.client
._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Cancel) ._unstable_cancelScheduledDelayedEvent(delayId)
.then(() => { .then(() => {
this.state.delayId = undefined; this.state.delayId = undefined;
this.resetRateLimitCounter(MembershipActionType.SendDelayedEvent); this.resetRateLimitCounter(MembershipActionType.SendDelayedEvent);
@@ -552,7 +555,7 @@ export class MembershipManager
}) })
.catch((e) => { .catch((e) => {
const repeatActionType = MembershipActionType.SendDelayedEvent; const repeatActionType = MembershipActionType.SendDelayedEvent;
const update = this.actionUpdateFromErrors(e, repeatActionType, "updateDelayedEvent"); const update = this.actionUpdateFromErrors(e, repeatActionType, "cancelScheduledDelayedEvent");
if (update) return update; if (update) return update;
if (this.isNotFoundError(e)) { if (this.isNotFoundError(e)) {
@@ -606,10 +609,7 @@ export class MembershipManager
// The obvious choice here would be to use the `IRequestOpts` to set the timeout. Since this call might be forwarded // The obvious choice here would be to use the `IRequestOpts` to set the timeout. Since this call might be forwarded
// to the widget driver this information would get lost. That is why we mimic the AbortError using the race. // to the widget driver this information would get lost. That is why we mimic the AbortError using the race.
return await Promise.race([ return await Promise.race([this.client._unstable_restartScheduledDelayedEvent(delayId), abortPromise])
this.client._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Restart),
abortPromise,
])
.then(() => { .then(() => {
// Whenever we successfully restart the delayed event we update the `state.expectedServerDelayLeaveTs` // Whenever we successfully restart the delayed event we update the `state.expectedServerDelayLeaveTs`
// which stores the predicted timestamp at which the server will send the delayed leave event if there wont be any further // which stores the predicted timestamp at which the server will send the delayed leave event if there wont be any further
@@ -637,7 +637,7 @@ export class MembershipManager
if (this.isUnsupportedDelayedEndpoint(e)) return {}; if (this.isUnsupportedDelayedEndpoint(e)) return {};
// TODO this also needs a test: get rate limit while checking id delayed event is scheduled // TODO this also needs a test: get rate limit while checking id delayed event is scheduled
const update = this.actionUpdateFromErrors(e, repeatActionType, "updateDelayedEvent"); const update = this.actionUpdateFromErrors(e, repeatActionType, "restartScheduledDelayedEvent");
if (update) return update; if (update) return update;
// In other error cases we have no idea what is happening // In other error cases we have no idea what is happening
@@ -647,7 +647,7 @@ export class MembershipManager
private async sendScheduledDelayedLeaveEventOrFallbackToSendLeaveEvent(delayId: string): Promise<ActionUpdate> { private async sendScheduledDelayedLeaveEventOrFallbackToSendLeaveEvent(delayId: string): Promise<ActionUpdate> {
return await this.client return await this.client
._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Send) ._unstable_sendScheduledDelayedEvent(delayId)
.then(() => { .then(() => {
this.state.hasMemberStateEvent = false; this.state.hasMemberStateEvent = false;
this.resetRateLimitCounter(MembershipActionType.SendScheduledDelayedLeaveEvent); this.resetRateLimitCounter(MembershipActionType.SendScheduledDelayedLeaveEvent);
@@ -661,7 +661,7 @@ export class MembershipManager
this.state.delayId = undefined; this.state.delayId = undefined;
return createInsertActionUpdate(repeatActionType); return createInsertActionUpdate(repeatActionType);
} }
const update = this.actionUpdateFromErrors(e, repeatActionType, "updateDelayedEvent"); const update = this.actionUpdateFromErrors(e, repeatActionType, "sendScheduledDelayedEvent");
if (update) return update; if (update) return update;
// On any other error we fall back to SendLeaveEvent (this includes hard errors from rate limiting) // On any other error we fall back to SendLeaveEvent (this includes hard errors from rate limiting)

View File

@@ -5513,10 +5513,10 @@ matrix-mock-request@^2.5.0:
dependencies: dependencies:
expect "^28.1.0" expect "^28.1.0"
matrix-widget-api@^1.10.0: matrix-widget-api@^1.14.0:
version "1.13.1" version "1.14.0"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.13.1.tgz#5b1caeed2fc58148bcd2984e0546d2d06a1713ad" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.14.0.tgz#aa90c40ace27d3165299f7dbc760a53001ce1446"
integrity sha512-mkOHUVzaN018TCbObfGOSaMW2GoUxOfcxNNlTVx5/HeMk3OSQPQM0C9oEME5Liiv/dBUoSrEB64V8wF7e/gb1w== integrity sha512-DDvZGOQhI/rilPWg5VlLN7pHIsPt0Jt14lsuHDP+KU+fmpAQNITJ6aIld1ZoXWsrVGv2PS3x6K/MHtfruIOQJQ==
dependencies: dependencies:
"@types/events" "^3.0.0" "@types/events" "^3.0.0"
events "^3.2.0" events "^3.2.0"