=encoding utf-8

=head1 NAME

WebSocket - WebSocket Javascript Class

=head1 SYNOPSIS

    var ws = new Perl.WebSocket( "wss://chat.example.org/v2",
    {
        max: 7, // max reconnect attempts
        onerror: function(event)
        {
            console.log( "WebSocket error observed:" + event );
        },
        onmaximum: function(event)
        {
            console.log( "Reached maximum retry to connect to the WebSocket server." );
        },
        onmessage: function(event)
        {
            console.log( "WebSocket message received:" + event );
        },
        onopen: function(event)
        {
            console.log( "WebSocket is open now." );
        },
        onreconnect: function()
        {
            console.log( "Attempting to reconnect to WebSocket server." );
        },
        protocol: ['some', 'stuff'],
        timeout: 5000 // 5 seconds
    });
    ws.send( "some raw data" );
    # Will be encapsulated into json before being sent
    ws.json( "some message" );

=head1 VERSION

    v0.1.0

=head1 DESCRIPTION

This Javascript class serves to make WebSocket connections. It uses the security principle of double authentication tokens as described L<here|https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html>.

Upon connection, an authentication token (previously set and provided by a remote server and stored in the local storage) will be passed as a cookie to the remote websocket server, as a query string to the connection url with the parameter C<csrf>

=head1 METHODS

=head2 new

Instantiate a new WebSocket object provided with the following options:

=over 4

=item I<max>

Integer. Maximum number of reconnect attempts. Defaults to infinity.

=item I<onclose>

    ws.onclose = function(event)
    {
        console.log( "WebSocket is closed now." );
    };

Optional function callback triggered when the server connection is closed. It "is called when the WebSocket connection's readyState changes to CLOSED"

The callback function is called with a L<CloseEvent object|https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent>.

The CloseEvent object "inherits properties from its parent, Event" and has the following properties, according to Mozilla:

=over 8

=item I<CloseEvent.code>

Read only. Returns an unsigned short containing the close code sent by the server.

=item I<CloseEvent.reason>

Read only. Returns a L<DOMString|https://developer.mozilla.org/en-US/docs/Web/API/DOMString> indicating the reason the server closed the connection. This is specific to the particular server and sub-protocol.

=item I<CloseEvent.wasClean>

Read only. Returns a boolean value that Indicates whether or not the connection was cleanly closed.

=back

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/onclose>

=item I<onerror>

    ws.onerror = function(event)
    {
        console.log( "WebSocket error observed:" + event );
    };

Optional function callback triggered "whenever an error event occurs on the WebSocket connection."

The callback function is called with the L<event object|https://developer.mozilla.org/en-US/docs/Web/API/Event>.

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/onerror>

=item I<onmaximum>

    ws.onmaximum = function(event)
    {
        console.log( "Reached maximum retry to connect to the WebSocket server." );
    }

Optional function callback triggered upon reconnect when the maximum attempts have been reached. This is not part of the WebScket api.

The callback function is called with the L<event object|https://developer.mozilla.org/en-US/docs/Web/API/Event>.

=item I<onmessage>

    ws.onmessage = function(json, event)
    {
        console.log( "WebSocket message received: " + JSON.stringify(json, null, 4) );
    };

Optional function callback triggered when a message is received from the server. "It is called with a MessageEvent."

The message from the server is expected to be a valid json data, which will be parsed and passed to the callback function.

A second parameter is passed, which is a L<MessageEvent object|https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent>.

The MessageEvent object "inherits properties from its parent, Event" and has the following properties, according to Mozilla:

=over 8

=item I<MessageEvent.data>

Read only. The data sent by the message emitter.

=item I<MessageEvent.origin>

Read only. A L<USVString|https://developer.mozilla.org/en-US/docs/Web/API/USVString> representing the origin of the message emitter.

=item I<MessageEvent.lastEventId>

Read only. A L<DOMString|https://developer.mozilla.org/en-US/docs/Web/API/DOMString> representing a unique ID for the event.

=item I<MessageEvent.source>

Read only. A MessageEventSource (which can be a WindowProxy, L<MessagePort|https://developer.mozilla.org/en-US/docs/Web/API/MessagePort>, or L<ServiceWorker|https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker> object) representing the message emitter.

=item I<MessageEvent.ports>

Read only. An array of L<MessagePort|https://developer.mozilla.org/en-US/docs/Web/API/MessagePort> objects representing the ports associated with the channel the message is being sent through (where appropriate, e.g. in channel messaging or when sending a message to a shared worker). 

=back

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/onmessage>

=item I<onopen>

    ws.onopen = function(event)
    {
        console.log( "WebSocket is open now." );
    };

Optional function callback triggered when the server connection is opened. It "is called when the WebSocket connection's readyState changes to 1; this indicates that the connection is ready to send and receive data."

The callback function is called with the L<event object|https://developer.mozilla.org/en-US/docs/Web/API/Event>.

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/onopen>

=item I<onreconnect>

    ws.onreconnect = function()
    {
        console.log( "Attempting to reconnect to WebSocket server." );
    }

Optional function callback triggered when trying to reconnect to the server, and before an connection attemept is made. This is not part of the WebScket api.

The callback function is called with the L<event object|https://developer.mozilla.org/en-US/docs/Web/API/Event>.

=item I<protocols>

    var protocol = ws.protocol;

Optional array of protocols. Default to empty array.

"The WebSocket.protocol read-only property returns the name of the sub-protocol the server selected; this will be one of the strings specified in the protocols parameter when creating the WebSocket object, or the empty string if no connection is established."

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/protocol>

=item I<timeout>

Optional integer value for timeout to reconnect.

=back

=head2 addEventListener( type, function )

Adds an event listener provided with C<function> for the given C<type>

=head2 close(code, reason)

    ws.close();

    ws.close(code);

    ws.close(reason);

    ws.close(code, reason);

Closes the connection.

Takes 2 parameters:

=over 4

=item I<code>

Optional. A numeric value indicating the status code explaining why the connection is being closed. If this parameter is not specified, a default value of 1005 is assumed. See the list of status codes of L<CloseEvent|https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent> for permitted values.

Known codes are:

=over 8

=item 1000

The default, normal closure (used if no code supplied),

"1000 indicates a normal closure, meaning that the purpose for which the connection was established has been fulfilled."

=item 1001

The party is going away, e.g. server is shutting down, or a browser leaves the page.

"1001 indicates that an endpoint is "going away", such as a server going down or a browser having navigated away from a page."

=item 1002

"1002 indicates that an endpoint is terminating the connection due to a protocol error."

=item 1003

"1003 indicates that an endpoint is terminating the connection because it has received a type of data it cannot accept (e.g., an endpoint that understands only text data MAY send this if it receives a binary message)."

=item 1004

Reserved for future use.

=item 1005

"1005 is a reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint. It is designated for use in applications expecting a status code to indicate that no status code was actually present."

=item 1006

No way to set such code manually, indicates that the connection was lost (no close frame).

"1006 is a reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint.  It is designated for use in applications expecting a status code to indicate that the connection was closed abnormally, e.g., without sending or receiving a Close control frame."

=item 1007

"1007 indicates that an endpoint is terminating the connection because it has received data within a message that was not consistent with the type of the message (e.g., non-UTF-8 [L<RFC3629|https://datatracker.ietf.org/doc/html/rfc3629>] data within a text message)."

=item 1008

"1008 indicates that an endpoint is terminating the connection because it has received a message that violates its policy. This is a generic status code that can be returned when there is no other more suitable status code (e.g., 1003 or 1009) or if there is a need to hide specific details about the policy."

=item 1009

The message is too big to process.

"1009 indicates that an endpoint is terminating the connection because it has received a message that is too big for it to process."

=item 1010

"1010 indicates that an endpoint (client) is terminating the connection because it has expected the server to negotiate one or more extension, but the server didn't return them in the response message of the WebSocket handshake. The list of extensions that are needed SHOULD appear in the /reason/ part of the Close frame. Note that this status code is not used by the server, because it can fail the WebSocket handshake instead."

=item 1011

Unexpected error on server.

"1011 indicates that a server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request."

=item 1015

"1015 is a reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint.  It is designated for use in applications expecting a status code to indicate that the connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified)."

=back

Full list of codes can be found at L<RFC6455, §7.4.1.|https://tools.ietf.org/html/rfc6455#section-7.4.1>

See L<rfc6455 documentation|https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5> for more information.

=item I<reason>

Optional. A human-readable string explaining why the connection is closing. This string must be no longer than 123 bytes of UTF-8 text (not characters).

See L<rfc6455 documentation|https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6> for more information.

=back

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close>

=head2 getCsrf()

Takes no arguments and returns the current csrf token stored in local storage, if any, otherwise returns C<undefined>

=head2 isClosed()

Returns true if the connection to the WebSocket server is closed, false otherwise.

=head2 isClosing()

Returns true if the connection to the WebSocket server is shutting down, false otherwise.

=head2 isConnecting()

Returns true if there is an ongoing connection attempt to the WebSocket server, false otherwise.

=head2 isConnected()

Returns true if connected to the WebSocket server, false otherwise.

=head2 json(data)

Provided with some json data, and this will add the current csrf token to json property C<csrf> and send the stringified json data to the WebSocket server.

=head2 makePropertyReadOnly(property)

Provided with a WebSocket C<property> and this will alias it to our AI WebSocket object, but make it read-only.

=head2 makePropertyReadWrite(property)

Provided with a WebSocket C<property> and this will alias it to our AI WebSocket object, and make it read-writable.

=head2 noop()

A default function that does absolutely nothing and is used when no function is provided.

=head2 open()

Initiiate a connection to the remove WebSocket server at the L</uri> specified during object instantiation.

It will retrieve the current csrf token from the local storage, if any, and add it as a query string to the connection url.

If it cannot connect, it will throw an error.

=head2 reconnect()

Attempts to reconnect until C<max> parameter is reached.

Upon every attempt, it will call the callback function set with C<onreconnect>

If C<max> attempts have been reached, it will call the callback function set with C<onmaximum>

=head2 send(data)

    ws.send( "Hello server!" );

Enqueues data to be transmitted.

"The WebSocket.send() method enqueues the specified data to be transmitted to the server over the WebSocket connection, increasing the value of bufferedAmount by the number of bytes needed to contain the data. If the data can't be sent (for example, because it needs to be buffered but the buffer is full), the socket is closed automatically."

The data may be one of the following data types:

=over 4

=item I<USVString>

A text string. The string is added to the buffer in UTF-8 format, and the value of bufferedAmount is increased by the number of bytes required to represent the UTF-8 string.

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/USVString>

=item I<ArrayBuffer>

You can send the underlying binary data used by a typed array object; its binary data contents are queued in the buffer, increasing the value of bufferedAmount by the requisite number of bytes.

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer>

=item I<Blob>

Specifying a Blob enqueues the blob's raw data to be transmitted in a binary frame. The value of bufferedAmount is increased by the byte size of that raw data.

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/Blob>

=item I<ArrayBufferView>

You can send any JavaScript typed array object as a binary frame; its binary data contents are queued in the buffer, increasing the value of bufferedAmount by the requisite number of bytes. 

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView>

=back

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send>

=head2 socket

Returns the actual WebSocket object.

=head1 PROPERTIES

=head2 binaryType

    // Create WebSocket connection.
    const socket = new Perl.WebSocket( "ws://localhost:8080" );
    // Change binary type from "blob" to "arraybuffer"
    socket.binaryType = "arraybuffer";

    // Listen for messages
    socket.addEventListener("message", function (event) {
        if(event.data instanceof ArrayBuffer) {
            // binary frame
            const view = new DataView(event.data);
            console.log(view.getInt32(0));
        } else {
            // text frame
            console.log(event.data);
        }
    });

"The binary data type used by the connection."

Possible values:

=over 4

=item I<blob>

Use Blob objects for binary data. This is the default value.

=item I<arraybuffer>

Use ArrayBuffer objects for binary data. 

=back

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType>

=head2 bufferedAmount

    var bufferedAmount = ws.bufferedAmount;

Read only. An unsigned long.

"The number of bytes of queued data."

"The WebSocket.bufferedAmount read-only property returns the number of bytes of data that have been queued using calls to send() but not yet transmitted to the network. This value resets to zero once all queued data has been sent. This value does not reset to zero when the connection is closed; if you keep calling send(), this will continue to climb."

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/bufferedAmount>

=head2 extensions

    var extensions = ws.extensions;

Read only. "The extensions selected by the server."

"The WebSocket.extensions read-only property returns the extensions selected by the server. This is currently only the empty string or a list of extensions as negotiated by the connection."

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/extensions>

=head2 protocol

    var protocol = ws.protocol;

Read only. "The sub-protocol selected by the server."

"The WebSocket.protocol read-only property returns the name of the sub-protocol the server selected; this will be one of the strings specified in the protocols parameter when creating the WebSocket object, or the empty string if no connection is established."

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/protocol>

=head2 readyState

Read only. "The current state of the connection."

"The WebSocket.readyState read-only property returns the current state of the WebSocket connection."

=begin html

<table>
  <thead>
    <tr>
      <th>Value</th>
      <th>State</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>0</code></td>
      <td><code>CONNECTING</code></td>
      <td>Socket has been created. The connection is not yet open.</td>
    </tr>
    <tr>
      <td><code>1</code></td>
      <td><code>OPEN</code></td>
      <td>The connection is open and ready to communicate.</td>
    </tr>
    <tr>
      <td><code>2</code></td>
      <td><code>CLOSING</code></td>
      <td>The connection is in the process of closing.</td>
    </tr>
    <tr>
      <td><code>3</code></td>
      <td><code>CLOSED</code></td>
      <td>The connection is closed or couldn't be opened.</td>
    </tr>
  </tbody>
</table>

=end html

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState>

=head2 url

    var url = ws.url;

Read only. "The absolute URL of the WebSocket."

"The WebSocket.url read-only property returns the absolute URL of the WebSocket as resolved by the constructor."

See for more information: L<https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/url> and L<https://datatracker.ietf.org/doc/html/rfc6455#page-14>

=head1 CONSTANTS

=head2 CONNECTING

0

=head2 OPEN

1

=head2 CLOSING

2

=head2 CLOSED

3

=head1 SEE ALSO

L<Mozila documentation|https://developer.mozilla.org/en-US/docs/Web/API/WebSocket>, L<JavaScript Info documentation|https://javascript.info/websocket>, L<rfc6455|https://datatracker.ietf.org/doc/html/rfc6455>, L<https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications>, L<https://javascript.info/websocket>

=head1 AUTHOR

Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>

=head1 COPYRIGHT & LICENSE

Copyright(c) 2021 DEGUEST Pte. Ltd. DEGUEST Pte. Ltd.

You can use, copy, modify and redistribute this package and associated files under the same terms as Perl itself.

=cut
