Skip to content

Commit

Permalink
feat: Support custom JSON message reviver and replacer (enisdenjo…
Browse files Browse the repository at this point in the history
…#172)

Co-authored-by: enisdenjo <badurinadenis@gmail.com>
  • Loading branch information
airhorns and enisdenjo committed Apr 29, 2021
1 parent 3dfa8db commit 0a9894e
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 26 deletions.
22 changes: 22 additions & 0 deletions docs/interfaces/client.clientoptions.md
Expand Up @@ -13,6 +13,8 @@ Configuration used for the GraphQL over WebSocket client.
- [connectionParams](client.clientoptions.md#connectionparams)
- [generateID](client.clientoptions.md#generateid)
- [isFatalConnectionProblem](client.clientoptions.md#isfatalconnectionproblem)
- [jsonMessageReplacer](client.clientoptions.md#jsonmessagereplacer)
- [jsonMessageReviver](client.clientoptions.md#jsonmessagereviver)
- [keepAlive](client.clientoptions.md#keepalive)
- [lazy](client.clientoptions.md#lazy)
- [on](client.clientoptions.md#on)
Expand Down Expand Up @@ -92,6 +94,26 @@ option.

___

### jsonMessageReplacer

`Optional` **jsonMessageReplacer**: [*JSONMessageReplacer*](../modules/common.md#jsonmessagereplacer)

An optional override for the JSON.stringify function used to serialize
outgoing messages from this client. Useful for serializing custom
datatypes out to the client.

___

### jsonMessageReviver

`Optional` **jsonMessageReviver**: [*JSONMessageReviver*](../modules/common.md#jsonmessagereviver)

An optional override for the JSON.parse function used to hydrate
incoming messages to this client. Useful for parsing custom datatypes
out of the incoming JSON.

___

### keepAlive

`Optional` **keepAlive**: *number*
Expand Down
22 changes: 22 additions & 0 deletions docs/interfaces/server.serveroptions.md
Expand Up @@ -17,6 +17,8 @@
- [connectionInitWaitTimeout](server.serveroptions.md#connectioninitwaittimeout)
- [context](server.serveroptions.md#context)
- [execute](server.serveroptions.md#execute)
- [jsonMessageReplacer](server.serveroptions.md#jsonmessagereplacer)
- [jsonMessageReviver](server.serveroptions.md#jsonmessagereviver)
- [onClose](server.serveroptions.md#onclose)
- [onComplete](server.serveroptions.md#oncomplete)
- [onConnect](server.serveroptions.md#onconnect)
Expand Down Expand Up @@ -94,6 +96,26 @@ in the close event reason.

___

### jsonMessageReplacer

`Optional` **jsonMessageReplacer**: [*JSONMessageReplacer*](../modules/common.md#jsonmessagereplacer)

An optional override for the JSON.stringify function used to serialize
outgoing messages to from server. Useful for serializing custom
datatypes out to the client.

___

### jsonMessageReviver

`Optional` **jsonMessageReviver**: [*JSONMessageReviver*](../modules/common.md#jsonmessagereviver)

An optional override for the JSON.parse function used to hydrate
incoming messages to this server. Useful for parsing custom datatypes
out of the incoming JSON.

___

### onClose

`Optional` **onClose**: (`ctx`: [*Context*](server.context.md)<E\>, `code`: *number*, `reason`: *string*) => *void* \| *Promise*<void\>
Expand Down
14 changes: 14 additions & 0 deletions docs/modules/client.md
Expand Up @@ -13,6 +13,8 @@
- [ErrorMessage](client.md#errormessage)
- [GRAPHQL\_TRANSPORT\_WS\_PROTOCOL](client.md#graphql_transport_ws_protocol)
- [ID](client.md#id)
- [JSONMessageReplacer](client.md#jsonmessagereplacer)
- [JSONMessageReviver](client.md#jsonmessagereviver)
- [Message](client.md#message)
- [MessageType](client.md#messagetype)
- [NextMessage](client.md#nextmessage)
Expand Down Expand Up @@ -259,6 +261,18 @@ Re-exports: [ID](common.md#id)

___

### JSONMessageReplacer

Re-exports: [JSONMessageReplacer](common.md#jsonmessagereplacer)

___

### JSONMessageReviver

Re-exports: [JSONMessageReviver](common.md#jsonmessagereviver)

___

### Message

Re-exports: [Message](common.md#message)
Expand Down
60 changes: 58 additions & 2 deletions docs/modules/common.md
Expand Up @@ -23,6 +23,8 @@
### Type aliases

- [ID](common.md#id)
- [JSONMessageReplacer](common.md#jsonmessagereplacer)
- [JSONMessageReviver](common.md#jsonmessagereviver)
- [Message](common.md#message)

### Variables
Expand All @@ -47,6 +49,58 @@ subscriptions established by the client.

___

### JSONMessageReplacer

Ƭ **JSONMessageReplacer**: (`this`: *any*, `key`: *string*, `value`: *any*) => *any*

Function that allows customization of the produced JSON string
for the elements of an outgoing `Message` object.

Read more about using it:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter

#### Type declaration:

▸ (`this`: *any*, `key`: *string*, `value`: *any*): *any*

#### Parameters:

| Name | Type |
| :------ | :------ |
| `this` | *any* |
| `key` | *string* |
| `value` | *any* |

**Returns:** *any*

___

### JSONMessageReviver

Ƭ **JSONMessageReviver**: (`this`: *any*, `key`: *string*, `value`: *any*) => *any*

Function for transforming values within a message during JSON parsing
The values are produced by parsing the incoming raw JSON.

Read more about using it:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#using_the_reviver_parameter

#### Type declaration:

▸ (`this`: *any*, `key`: *string*, `value`: *any*): *any*

#### Parameters:

| Name | Type |
| :------ | :------ |
| `this` | *any* |
| `key` | *string* |
| `value` | *any* |

**Returns:** *any*

___

### Message

Ƭ **Message**<T\>: T *extends* [*ConnectionAck*](../enums/common.messagetype.md#connectionack) ? [*ConnectionAckMessage*](../interfaces/common.connectionackmessage.md) : T *extends* [*ConnectionInit*](../enums/common.messagetype.md#connectioninit) ? [*ConnectionInitMessage*](../interfaces/common.connectioninitmessage.md) : T *extends* [*Subscribe*](../enums/common.messagetype.md#subscribe) ? [*SubscribeMessage*](../interfaces/common.subscribemessage.md) : T *extends* [*Next*](../enums/common.messagetype.md#next) ? [*NextMessage*](../interfaces/common.nextmessage.md) : T *extends* [*Error*](../enums/common.messagetype.md#error) ? [*ErrorMessage*](../interfaces/common.errormessage.md) : T *extends* [*Complete*](../enums/common.messagetype.md#complete) ? [*CompleteMessage*](../interfaces/common.completemessage.md) : *never*
Expand Down Expand Up @@ -85,7 +139,7 @@ ___

### parseMessage

**parseMessage**(`data`: *unknown*): [*Message*](common.md#message)
**parseMessage**(`data`: *unknown*, `reviver?`: [*JSONMessageReviver*](common.md#jsonmessagereviver)): [*Message*](common.md#message)

Parses the raw websocket message data to a valid message.

Expand All @@ -94,14 +148,15 @@ Parses the raw websocket message data to a valid message.
| Name | Type |
| :------ | :------ |
| `data` | *unknown* |
| `reviver?` | [*JSONMessageReviver*](common.md#jsonmessagereviver) |

**Returns:** [*Message*](common.md#message)

___

### stringifyMessage

**stringifyMessage**<T\>(`msg`: [*Message*](common.md#message)<T\>): *string*
**stringifyMessage**<T\>(`msg`: [*Message*](common.md#message)<T\>, `replacer?`: [*JSONMessageReplacer*](common.md#jsonmessagereplacer)): *string*

Stringifies a valid message ready to be sent through the socket.

Expand All @@ -116,5 +171,6 @@ Stringifies a valid message ready to be sent through the socket.
| Name | Type |
| :------ | :------ |
| `msg` | [*Message*](common.md#message)<T\> |
| `replacer?` | [*JSONMessageReplacer*](common.md#jsonmessagereplacer) |

**Returns:** *string*
59 changes: 42 additions & 17 deletions src/client.ts
Expand Up @@ -14,6 +14,8 @@ import {
parseMessage,
stringifyMessage,
SubscribePayload,
JSONMessageReviver,
JSONMessageReplacer,
} from './common';
import { isObject } from './utils';

Expand Down Expand Up @@ -236,6 +238,18 @@ export interface ClientOptions {
* Reference: https://gist.github.com/jed/982883
*/
generateID?: () => ID;
/**
* An optional override for the JSON.parse function used to hydrate
* incoming messages to this client. Useful for parsing custom datatypes
* out of the incoming JSON.
*/
jsonMessageReviver?: JSONMessageReviver;
/**
* An optional override for the JSON.stringify function used to serialize
* outgoing messages from this client. Useful for serializing custom
* datatypes out to the client.
*/
jsonMessageReplacer?: JSONMessageReplacer;
}

/** @category Client */
Expand Down Expand Up @@ -298,6 +312,8 @@ export function createClient(options: ClientOptions): Client {
return v.toString(16);
});
},
jsonMessageReplacer: replacer,
jsonMessageReviver: reviver,
} = options;

let ws;
Expand Down Expand Up @@ -412,13 +428,16 @@ export function createClient(options: ClientOptions): Client {
socket.onopen = async () => {
try {
socket.send(
stringifyMessage<MessageType.ConnectionInit>({
type: MessageType.ConnectionInit,
payload:
typeof connectionParams === 'function'
? await connectionParams()
: connectionParams,
}),
stringifyMessage<MessageType.ConnectionInit>(
{
type: MessageType.ConnectionInit,
payload:
typeof connectionParams === 'function'
? await connectionParams()
: connectionParams,
},
replacer,
),
);
} catch (err) {
socket.close(
Expand All @@ -431,7 +450,7 @@ export function createClient(options: ClientOptions): Client {
let acknowledged = false;
socket.onmessage = ({ data }) => {
try {
const message = parseMessage(data);
const message = parseMessage(data, reviver);
emitter.emit('message', message);
if (acknowledged) return; // already connected and acknowledged

Expand Down Expand Up @@ -600,21 +619,27 @@ export function createClient(options: ClientOptions): Client {
});

socket.send(
stringifyMessage<MessageType.Subscribe>({
id,
type: MessageType.Subscribe,
payload,
}),
stringifyMessage<MessageType.Subscribe>(
{
id,
type: MessageType.Subscribe,
payload,
},
replacer,
),
);

releaser = () => {
if (!done && socket.readyState === WebSocketImpl.OPEN)
// if not completed already and socket is open, send complete message to server on release
socket.send(
stringifyMessage<MessageType.Complete>({
id,
type: MessageType.Complete,
}),
stringifyMessage<MessageType.Complete>(
{
id,
type: MessageType.Complete,
},
replacer,
),
);
locks--;
done = true;
Expand Down
32 changes: 29 additions & 3 deletions src/common.ts
Expand Up @@ -190,35 +190,61 @@ export function isMessage(val: unknown): val is Message {
return false;
}

/**
* Function for transforming values within a message during JSON parsing
* The values are produced by parsing the incoming raw JSON.
*
* Read more about using it:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#using_the_reviver_parameter
*
* @category Common
*/
export type JSONMessageReviver = (this: any, key: string, value: any) => any; // eslint-disable-line @typescript-eslint/no-explicit-any

/**
* Parses the raw websocket message data to a valid message.
*
* @category Common
*/
export function parseMessage(data: unknown): Message {
export function parseMessage(
data: unknown,
reviver?: JSONMessageReviver,
): Message {
if (isMessage(data)) {
return data;
}
if (typeof data !== 'string') {
throw new Error('Message not parsable');
}
const message = JSON.parse(data);
const message = JSON.parse(data, reviver);
if (!isMessage(message)) {
throw new Error('Invalid message');
}
return message;
}

/**
* Function that allows customization of the produced JSON string
* for the elements of an outgoing `Message` object.
*
* Read more about using it:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter
*
* @category Common
*/
export type JSONMessageReplacer = (this: any, key: string, value: any) => any; // eslint-disable-line @typescript-eslint/no-explicit-any

/**
* Stringifies a valid message ready to be sent through the socket.
*
* @category Common
*/
export function stringifyMessage<T extends MessageType>(
msg: Message<T>,
replacer?: JSONMessageReplacer,
): string {
if (!isMessage(msg)) {
throw new Error('Cannot stringify invalid message');
}
return JSON.stringify(msg);
return JSON.stringify(msg, replacer);
}

0 comments on commit 0a9894e

Please sign in to comment.