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
getandsetmethods 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
setmethods should returnthisin 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_patchproperties. See/lib/js/src/util/Version.jsin 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 aResponsesuffix. 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, orRpcNotificationclass (depending on theirmessagetypein the RPC Spec) and be stored in/lib/js/src/rpc/messages - RPC structs shall extend the
RpcStructclass and be stored in/lib/js/src/rpc/structs - RPC enums shall extend the
Enumclass 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
_MAPkeys and static getters of theFunctionIDenum shall not include theIDsuffix. e.g.RegisterAppInterfaceID -> RegisterAppInterface. All references to theFunctionIDenum shall adopt this as well to ensure valid references. setmethods 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
booleanshould 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
functionkeyword, 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
elseon the same line as the closingifcurly 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
switchstatements 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: 'hello@example.com',
});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: ...