Note: This is a living document managed by the SDL Project Maintainer and may change as new questions and/or discoveries arise during technical implementation.
Common library files shall be stored in the /lib/js/src
directory, grouped into subdirectories by their logic type (e.g. protocol, transport, session, rpc, manager, util), similar to the Java Suite. Compressed distributions of the library shall be written to /lib/js/dist/SDL.js
.
Node.js library files shall be stored in the /lib/node/src
directory, grouped into subdirectories in the same way as common files.
Example projects shall be located in the /examples/js
and /examples/node
directories, respectively.
- Concepts must be broken down into classes for an object-oriented programming methodology.
- Although ECMAScript does not yet officially support private properties, properties shall be named with an underscore prefix to allow for an easy transition to private properties when they become available.
- Rather than promoting the direct manipulation of property values, the use of prefixed
get
andset
methods shall be used. For example:setMajor(1)
andgetMajor()
. - The use of ES6-style class "getters" and "setters" shall be avoided. For example:
static get major () { return this._major; }
andstatic set major (val) { this._major = val; }
. An exception to this rule is made for enumerations (see "Enumerations" section below). - All
set
methods should returnthis
in order to support method chaining. - Classes must contain a constructor, but the constructor should not contain parameter type/value checking as a workaround for method overloading. For example, a semantic versioning class should have a constructor which accepts 3 parameters (major, minor, patch) and assigned to properties of the same/similar names prefixed with
_
. To also allow a semantic version string (e.g. "2.1.3") as input, a newfromString(version)
method should be added to the class definition which parses the string and sets the proper_major
,_minor
, and_patch
properties. See/lib/js/src/util/Version.js
in the repository for an example. - Constructor parameters should be optional in most cases to support utilizing class methods as a helper without the need to initialize an instance with valid parameters.
- File names shall match the class name defined in the file (including capitalization), unless otherwise noted in this document.
Since JavaScript does not offer traditional enumerations, an abstract Enum
class has been defined (see /lib/js/src/util/Enum.js
) which shall be extended for the purpose of creating new enumerations. Each extended enumeration class must contain a constructor which calls super()
and contain a static valueForString(key)
method which returns a given enumeration value if the provided key exists in the collection (otherwise null
) using the _valueForString(key)
private method found in the base Enum
class. See /lib/js/src/transport/enums/TransportType.js
for an example. Special rules exist for enumerations contained in the RPC Spec (see "RPC Class Generation" section below).
Each enumeration class shall contain a private _MAP
property defined as a frozen Object of key-value pairs. If an enumeration element in the RPC Spec contains an internal_name
attribute, that value shall be used as the corresponding _MAP
object key, otherwise the name
shall be used as the key. The value of the pair shall be set to the value of the hexvalue
attribute (if available), otherwise the name
attribute.
ES6-style getters as static
methods shall be used in enum classes to support dot-access notation of _MAP
values. The name of the getter methods shall always match the key of the corresponding _MAP
attribute and follow the name/internal_name rules outlined above.
All RPC messages, RPC structs, and RPC enumerations, unless otherwise noted in this guidelines document, shall be created using an automated generation script which parses the RPC Spec in accordance to the RPC Generation Proposal.
The following special rules shall apply to the RPC generation script for the JavaScript library:
- RPC messages of
messagetype="response"
in the RPC Spec shall generate files named with aResponse
suffix. e.g.PutFileResponse.js
- RPC messages of
messagetype="request"
in the RPC Spec shall generate files named without a suffix. e.g.PutFile.js
- RPC message parameter key constants shall match the RPC Spec to ensure proper functionality
- RPC messages shall extend the
RpcRequest
,RpcResponse
, orRpcNotification
class (depending on theirmessagetype
in the RPC Spec) and be stored in/lib/js/src/rpc/messages
- RPC structs shall extend the
RpcStruct
class and be stored in/lib/js/src/rpc/structs
- RPC enums shall extend the
Enum
class and be stored in/lib/js/src/rpc/enums
- Uses of the "sync" prefix shall be replaced with "sdl" (where it would not break functionality). E.g.
SyncMsgVersion -> SdlMsgVersion
. This applies to member variables and their accessors. The key used when creating the RPC message JSON should match that of the RPC Spec. - The
_MAP
keys and static getters of theFunctionID
enum shall not include theID
suffix. e.g.RegisterAppInterfaceID -> RegisterAppInterface
. All references to theFunctionID
enum shall adopt this as well to ensure valid references. set
methods of RPC messages and RPC structs shall validate the type of the passed parameter by immediately callingthis._validateType([targetClass], [parameter]);
, followed by storing the passed parameter or object. Target classes used for validation must be imported into each respective message/struct file immediately following any copyright notice block comments.- Each extended RPC enumeration class must be named to match the enum name in the RPC Spec
All common files (that is, files strictly built upon ECMA-Script 2017) shall export their components via the named convention (e.g. export { BaseTransport };
). Likewise, imports shall always use the named convention and use relative file paths (e.g. import { BaseTransport } from '../transport/BaseTransport.js';
).
To utilize the common JavaScript library, you must first execute the build process to create a distribution file by running npm run build
from the root directory. The generated distribution file (located at /lib/js/dist/SDL.js
) is compatible with both vanilla JavaScript and Node.js.
For Node.js-specific components, ES module syntax shall be used. For example, module.exports = WebsocketServerTransport;
for exports; const SDL = require('../../../js/dist/SDL.js');
for imports. See /lib/node/src/transport/WebsocketServerTransport.js
for a brief example of importing the common SDL.js library, extending it, and exporting the extension as an ES module.
For event-driven tasks which are called many times throughout an application's lifecycle, such as connection listeners, function passing shall be used. For example, the TransportListener
class method onPacketReceived
will be executed multiple times during an application's lifecycle, and should therefore pass a function to be called upon a triggering event.
In cases where logic must be chained, Promises shall be used. For example, setting an application icon requires to first send a PutFile
RPC, followed by a SetAppIcon
RPC. Since these commands must happen sequentially, a Promise should be used to avoid creating nested callbacks.
All methods must be documented immediately prior to their declaration using the following format:
/**
* [Description of what the method does].
* @param {data_type} [param_name] - [Description of the parameter].
* @returns {data_type} - [Description of the returned data].
*/
Class constructors must be documented immediately prior to their declaration using the following format:
/**
* [Description of what the constructor does].
* @class
* @param {data_type} [param_name] - [Description of the parameter].
*/
All contributions must pass lint validation to be accepted. Lint validation can be performed on the entire project via npm run lint
.
Use const
or let
for references (block-scoped). Avoid the use of var
(function-scoped). eslint: prefer-const
, no-const-assign
, no-var
const one = 1;
let two = 2;
Group const
references together, followed by your let
references.
const one = 1;
const two = 2;
const three = 3;
const four = 4;
let name = 'Livio';
let website = 'livio.io';
Use dot notation to access object properties. eslint: dot-notation
const thing = {
one: 1,
two: 2,
three: 3,
};
let result = thing.one * thing.two;
Use bracket notation to dynamically access object properties.
const thing = {
one: 1,
two: 2,
three: 3,
};
let key = 'one';
let result = thing[key];
- Avoid the use of single-letter names. eslint
id-length
. - Objects, functions, and instances should use camelCase.
- Exported constants and constant class properties should be entirely uppercased.
- Class names shall use PascalCase. eslint:
new-cap
- Acronyms and abbreviations shall use camelCase or PascalCase in accordance with the other naming rules.
- Private class properties shall be indicated by using an underscore to prefix their name. These properties are still technically publicly accessible, but should not be directly accessed outside of the scope of their class definition.
- Properties and methods which are
boolean
should be prefixed withis
,can
, orhas
. e.g.isEnabled()
andhasTitle()
Please use them. eslint: semi
Trailing commas should be used, even for the last item in an array or object. Do not use leading commas. eslint: comma-style
, comma-dangle
let thing = {
one: 1,
two: 2,
three: 3,
timesFour(param) {
return param * 4;
},
};
- Tabs should not be used. 4 consecutive spaces should be used in lieu of a tab. eslint:
indent
- Function signatures should include a single space after the
function
keyword, the function name, and closing parenthesis. e.g.function a (b) { ... }
. eslint:space-before-function-paren
,space-before-blocks
- Use a single space before the opening parenthesis and after the closing parenthesis in control statements. e.g.
if (isEnabled) { ... }
eslint:keyword-spacing
- Pad curly braces with spaces on each side. e.g.
const thing = { key: 'value' };
eslint:object-curly-spacing
- Square brackets should not be padded with spaces. e.g.
const things = [1, 2, 3];
eslint:array-bracket-spacing
- Do not pad block internals with blank lines. eslint:
padded-blocks
- Leave a blank line between blocks to separate logic concepts.
- Use curly braces with multi-line blocks, but not single-line blocks. eslint:
nonblock-statement-body-position
- In an if-else statement, put
else
on the same line as the closingif
curly brace, e.g.} else {
. eslint:brace-style
- If control statement conditions exceed the maximum line length or become difficult to read, conditions should be grouped and separated onto new lines, each line starting with a logical operator.
- Enclose operators with parentheses when mixing them so the logic can be easily understood by other developers. eslint:
no-mixed-operators
- Do not nest ternary operations, or use unneeded ternaries. eslint:
no-nested-ternary
,no-unneeded-ternary
- Use curly braces in case/default handlers of
switch
statements to create logic blocks. eslint:no-case-declarations
- Shortcut comparisons (e.g.
if (isEnabled) { ... }
for booleans are acceptable, but explicit comparisons should be used for strings and numbers (e.g.if (things.length > 0) { ... }
. - Use strict comparison operators, e.g.
===
instead of==
. eslint:eqeqeq
Use literal syntax to create objects and arrays. eslint: no-new-object
let thing = {};
let things = [];
Adding items to an array should always be done via .push(item)
.
let things = [];
things.push({
name: 'one',
email: '[email protected]',
});
Use short-hand object methods. eslint: object-shorthand
const thing = {
one: 1,
genTwo(param) {
return obj.one + param;
},
}
Property keys should not be wrapped in quotes unless required.
const thing = {
one: 1,
two: 2,
'special-thing': 'should be avoided',
}
- Use single quotes for strings, e.g.
'example'
. eslint:quotes
- Do not use escaped line breaks or string concatenation to make long strings more readable. Instead, use your editor's text-wrapping option.
- Use template strings when inserting dynamic values into a string, e.g.
Hello, ${name}!
. eslint:prefer-template
,template-curly-spacing
- Only escape characters in a string when necessary. eslint:
no-useless-escape
Event listener registrars should always accept a function reference as input which is executed upon the event being emitted. The event method should be named with a prefix of on
.
function onEventListener(runThis) {
runThis(payload);
}
- Use
/** ... */
for multi-line comments - Use
//
for single-line comments - All comment lines should start with a space (as to not be back-to back with a
*
or/
character). eslint:spaced-comment
- To make note of an identified problem, use
// FIXME: ...
- To make note of an identified future solution, use
// TODO: ...