Skip to content

Commit

Permalink
feat: Send optional payload with the ConnectionAck message (enisden…
Browse files Browse the repository at this point in the history
…jo#60)

* feat: optional payload in ack

* feat: provide payload from onconnect

* feat: pass payload to event listener on client

* test: server

* test: client
  • Loading branch information
enisdenjo committed Nov 7, 2020
1 parent a54327c commit 1327e77
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 20 deletions.
3 changes: 3 additions & 0 deletions PROTOCOL.md
Expand Up @@ -45,9 +45,12 @@ Direction: **Server -> Client**

Expected response to the `ConnectionInit` message from the client acknowledging a successful connection with the server.

The server can use the optional `payload` field to transfer additional details about the connection.

```typescript
interface ConnectionAckMessage {
type: 'connection_ack';
payload?: Record<string, unknown>;
}
```

Expand Down
7 changes: 7 additions & 0 deletions docs/interfaces/_message_.connectionackmessage.md
Expand Up @@ -12,10 +12,17 @@

### Properties

* [payload](_message_.connectionackmessage.md#payload)
* [type](_message_.connectionackmessage.md#type)

## Properties

### payload

`Optional` `Readonly` **payload**: Record\<string, unknown>

___

### type

`Readonly` **type**: [ConnectionAck](../enums/_message_.messagetype.md#connectionack)
7 changes: 6 additions & 1 deletion docs/interfaces/_server_.serveroptions.md
Expand Up @@ -112,7 +112,7 @@ ___

### onConnect

`Optional` **onConnect**: undefined \| (ctx: [Context](_server_.context.md)) => Promise\<boolean \| void> \| boolean \| void
`Optional` **onConnect**: undefined \| (ctx: [Context](_server_.context.md)) => Promise\<Record\<string, unknown> \| boolean \| void> \| Record\<string, unknown> \| boolean \| void

Is the connection callback called when the
client requests the connection initialisation
Expand All @@ -128,6 +128,11 @@ allow the client to connect.
terminate the socket by dispatching the
close event `4403: Forbidden`.

- Returning a `Record` from the callback will
allow the client to connect and pass the returned
value to the client through the optional `payload`
field in the `ConnectionAck` message.

Throwing an error from within this function will
close the socket with the `Error` message
in the close event reason.
Expand Down
11 changes: 7 additions & 4 deletions docs/modules/_client_.md
Expand Up @@ -58,11 +58,14 @@ ___

### EventConnectedListener

Ƭ **EventConnectedListener**: (socket: unknown) => void
Ƭ **EventConnectedListener**: (socket: unknown, payload?: Record\<string, unknown>) => void

The argument is actually the `WebSocket`, but to avoid bundling DOM typings
because the client can run in Node env too, you should assert
the websocket type during implementation.
The first argument is actually the `WebSocket`, but to avoid
bundling DOM typings because the client can run in Node env too,
you should assert the websocket type during implementation.

Also, the second argument is the optional payload that the server may
send through the `ConnectionAck` message.

___

Expand Down
16 changes: 11 additions & 5 deletions src/client.ts
Expand Up @@ -21,11 +21,17 @@ export type EventClosed = 'closed';
export type Event = EventConnecting | EventConnected | EventClosed;

/**
* The argument is actually the `WebSocket`, but to avoid bundling DOM typings
* because the client can run in Node env too, you should assert
* the websocket type during implementation.
* The first argument is actually the `WebSocket`, but to avoid
* bundling DOM typings because the client can run in Node env too,
* you should assert the websocket type during implementation.
*
* Also, the second argument is the optional payload that the server may
* send through the `ConnectionAck` message.
*/
export type EventConnectedListener = (socket: unknown) => void;
export type EventConnectedListener = (
socket: unknown,
payload?: Record<string, unknown>,
) => void;

export type EventConnectingListener = () => void;

Expand Down Expand Up @@ -321,7 +327,7 @@ export function createClient(options: ClientOptions): Client {

clearTimeout(tooLong);
state = { ...state, acknowledged: true, socket, tries: 0 };
emitter.emit('connected', socket); // connected = socket opened + acknowledged
emitter.emit('connected', socket, message.payload); // connected = socket opened + acknowledged
return resolve();
} catch (err) {
socket.close(4400, err);
Expand Down
8 changes: 7 additions & 1 deletion src/message.ts
Expand Up @@ -31,6 +31,7 @@ export interface ConnectionInitMessage {

export interface ConnectionAckMessage {
readonly type: MessageType.ConnectionAck;
readonly payload?: Record<string, unknown>;
}

export interface SubscribeMessage {
Expand Down Expand Up @@ -95,7 +96,12 @@ export function isMessage(val: unknown): val is Message {
isObject(val.payload)
);
case MessageType.ConnectionAck:
return true;
// the connection ack message can have optional payload object too
return (
!hasOwnProperty(val, 'payload') ||
val.payload === undefined ||
isObject(val.payload)
);
case MessageType.Subscribe:
return (
hasOwnStringProperty(val, 'id') &&
Expand Down
36 changes: 27 additions & 9 deletions src/server.ts
Expand Up @@ -164,11 +164,22 @@ export interface ServerOptions {
* terminate the socket by dispatching the
* close event `4403: Forbidden`.
*
* - Returning a `Record` from the callback will
* allow the client to connect and pass the returned
* value to the client through the optional `payload`
* field in the `ConnectionAck` message.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
onConnect?: (ctx: Context) => Promise<boolean | void> | boolean | void;
onConnect?: (
ctx: Context,
) =>
| Promise<Record<string, unknown> | boolean | void>
| Record<string, unknown>
| boolean
| void;
/**
* The subscribe callback executed right after
* acknowledging the request before any payload
Expand Down Expand Up @@ -494,16 +505,23 @@ export function createServer(
ctx.connectionParams = message.payload;
}

if (onConnect) {
const permitted = await onConnect(ctx);
if (permitted === false) {
return ctx.socket.close(4403, 'Forbidden');
}
const permittedOrPayload = await onConnect?.(ctx);
if (permittedOrPayload === false) {
return ctx.socket.close(4403, 'Forbidden');
}

await sendMessage<MessageType.ConnectionAck>(ctx, {
type: MessageType.ConnectionAck,
});
await sendMessage<MessageType.ConnectionAck>(
ctx,
isObject(permittedOrPayload)
? {
type: MessageType.ConnectionAck,
payload: permittedOrPayload,
}
: {
type: MessageType.ConnectionAck,
// payload is completely absent if not provided
},
);

ctx.acknowledged = true;
break;
Expand Down
21 changes: 21 additions & 0 deletions src/tests/client.ts
Expand Up @@ -154,6 +154,27 @@ it('should not accept invalid WebSocket implementations', async () => {
).toThrow();
});

it('should recieve optional connection ack payload in event handler', async (done) => {
const { url } = await startTServer({
onConnect: () => ({ itsa: 'me' }),
});

createClient({
url,
lazy: false,
on: {
connected: (_socket, payload) => {
try {
expect(payload).toEqual({ itsa: 'me' });
} catch (err) {
fail(err);
}
done();
},
},
});
});

describe('query operation', () => {
it('should execute the query, "next" the result and then complete', async () => {
const { url } = await startTServer();
Expand Down
24 changes: 24 additions & 0 deletions src/tests/server.ts
Expand Up @@ -569,6 +569,30 @@ describe('Connect', () => {
await test(server.url);
});

it('should send optional payload with connection ack message', async () => {
const { url } = await startTServer({
onConnect: () => {
return {
itsa: 'me',
};
},
});

const client = await createTClient(url);
client.ws.send(
stringifyMessage<MessageType.ConnectionInit>({
type: MessageType.ConnectionInit,
}),
);

await client.waitForMessage(({ data }) => {
expect(parseMessage(data)).toEqual({
type: MessageType.ConnectionAck,
payload: { itsa: 'me' },
});
});
});

it('should pass in the `connectionParams` through the context and have other flags correctly set', async (done) => {
const connectionParams = {
some: 'string',
Expand Down

0 comments on commit 1327e77

Please sign in to comment.