Skip to content

Commit

Permalink
feat: add promise-based acknowledgements
Browse files Browse the repository at this point in the history
This commit adds some syntactic sugar around acknowledgements:

```js
// without timeout
const response = await socket.emitWithAck("hello", "world");

// with a specific timeout
try {
  const response = await socket.timeout(1000).emitWithAck("hello", "world");
} catch (err) {
  // the server did not acknowledge the event in the given delay
}
```

Note: enviroments that do not support Promises ([1]) will need to add a
polyfill in order to use this feature

See also: socketio/socket.io@184f3cf

[1]: https://caniuse.com/promises
  • Loading branch information
darrachequesne committed Jan 30, 2023
1 parent b4e20c5 commit 47b979d
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 1 deletion.
42 changes: 42 additions & 0 deletions lib/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ export type DecorateAcknowledgements<E> = {
: E[K];
};

export type Last<T extends any[]> = T extends [...infer H, infer L] ? L : any;
export type AllButLast<T extends any[]> = T extends [...infer H, infer L]
? H
: any[];
export type FirstArg<T> = T extends (arg: infer Param) => infer Result
? Param
: any;

export interface SocketOptions {
/**
* the authentication payload sent when connecting to the Namespace
Expand Down Expand Up @@ -407,6 +415,40 @@ export class Socket<
};
}

/**
* Emits an event and waits for an acknowledgement
*
* @example
* // without timeout
* const response = await socket.emitWithAck("hello", "world");
*
* // with a specific timeout
* try {
* const response = await socket.timeout(1000).emitWithAck("hello", "world");
* } catch (err) {
* // the server did not acknowledge the event in the given delay
* }
*
* @return a Promise that will be fulfilled when the server acknowledges the event
*/
public emitWithAck<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: AllButLast<EventParams<EmitEvents, Ev>>
): Promise<FirstArg<Last<EventParams<EmitEvents, Ev>>>> {
// the timeout flag is optional
const withErr = this.flags.timeout !== undefined;
return new Promise((resolve, reject) => {
args.push((arg1, arg2) => {
if (withErr) {
return arg1 ? reject(arg1) : resolve(arg2);
} else {
return resolve(arg1);
}
});
this.emit(ev, ...(args as any[] as EventParams<EmitEvents, Ev>));
});
}

/**
* Sends a packet.
*
Expand Down
38 changes: 38 additions & 0 deletions test/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,17 @@ describe("socket", () => {
});
});

it("should emit an event and wait for the acknowledgement", () => {
return wrap(async (done) => {
const socket = io(BASE_URL, { forceNew: true });

const val = await socket.emitWithAck("echo", 123);
expect(val).to.be(123);

success(done, socket);
});
});

describe("volatile packets", () => {
it("should discard a volatile packet when the socket is not connected", () => {
return wrap((done) => {
Expand Down Expand Up @@ -561,5 +572,32 @@ describe("socket", () => {
});
});
});

it("should timeout when the server does not acknowledge the event (promise)", () => {
return wrap(async (done) => {
const socket = io(BASE_URL + "/");

try {
await socket.timeout(50).emitWithAck("unknown");
expect.fail();
} catch (e) {
success(done, socket);
}
});
});

it("should not timeout when the server does acknowledge the event (promise)", () => {
return wrap(async (done) => {
const socket = io(BASE_URL + "/");

try {
const value = await socket.timeout(50).emitWithAck("echo", 42);
expect(value).to.be(42);
success(done, socket);
} catch (e) {
expect.fail();
}
});
});
});
});
44 changes: 43 additions & 1 deletion test/typed-events.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { io, Socket } from "..";
import type { DefaultEventsMap } from "@socket.io/component-emitter";
import { expectError, expectType } from "tsd";
import { createServer } from "http";

// This file is run by tsd, not mocha.

Expand Down Expand Up @@ -54,7 +55,7 @@ describe("typed events", () => {
});

describe("emit", () => {
it("accepts any parameters", () => {
it("accepts any parameters", async () => {
const socket = io();

socket.emit("random", 1, "2", [3]);
Expand All @@ -72,6 +73,24 @@ describe("typed events", () => {
});
});
});

describe("emitWithAck", () => {
it("accepts any parameters", async () => {
const socket = io();

const value = await socket.emitWithAck(
"ackFromClientSingleArg",
"1",
2
);
expectType<any>(value);

const value2 = await socket
.timeout(1000)
.emitWithAck("ackFromClientSingleArg", "3", 4);
expectType<any>(value2);
});
});
});

describe("single event map", () => {
Expand Down Expand Up @@ -127,6 +146,11 @@ describe("typed events", () => {
b: number,
ack: (c: string, d: boolean) => void
) => void;
ackFromClientSingleArg: (
a: string,
b: number,
ack: (c: string) => void
) => void;
ackFromClientNoArg: (ack: () => void) => void;
}

Expand Down Expand Up @@ -189,5 +213,23 @@ describe("typed events", () => {
expectError(socket.emit("wrong name"));
});
});

describe("emitWithAck", () => {
it("accepts arguments of the correct types", async () => {
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io();

const value = await socket.emitWithAck(
"ackFromClientSingleArg",
"1",
2
);
expectType<string>(value);

const value2 = await socket
.timeout(1000)
.emitWithAck("ackFromClientSingleArg", "3", 4);
expectType<string>(value2);
});
});
});
});

0 comments on commit 47b979d

Please sign in to comment.