Socket
Socket
Sign inDemoInstall

nogap

Package Overview
Dependencies
7
Maintainers
1
Versions
51
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.4.4 to 0.5.0

assets/bluebird.min.js

215

_README.md

@@ -6,8 +6,7 @@ [![NPM version](https://badge.fury.io/js/nogap.svg)](http://badge.fury.io/js/nogap)

The NoGap framework delivers [RPC (Remote Procedure Call)](http://en.wikipedia.org/wiki/Remote_procedure_call) + improved code sharing + asset management + some other good stuff for enjoyable Host <-> Client architecture development.
NoGap is a full-stack (spans Host and Client) JavaScript framework, featuring [RPC (Remote Procedure Calls)](http://en.wikipedia.org/wiki/Remote_procedure_call) + simple code sharing + basic asset management + full-stack [Promise chains](https://github.com/petkaantonov/bluebird#what-are-promises-and-why-should-i-use-them).
NoGap's primary use case is development of rich single-page, client-side applications while alleviating the typical hassles of doing so.
NoGap's primary use case is development of rich single-page web applications while alleviating the typical hassles of doing so.
This module is called `No` `Gap` because it removes the typical gap that exists between
host and client and that makes a Client <-> Server architecture so cumbersome to develop.
This module is called `No` `Gap` because it removes the typical gap that exists between Host and Client and that makes a client-server-architecture so cumbersome to develop.

@@ -18,3 +17,3 @@ You probably want to start by having a look at the [Samples](#samples) for reference.

When starting on a new component, you can save a bit of time by copying the [typical component skeleton code](#component_skeleton) from the [Structure of NoGap components](#component_structure) section.
The [Structure of NoGap components](#component_structure) section lays out the structure of NoGap's basic building block: the component.

@@ -126,8 +125,9 @@ Note that currently, the only dependency of NoGap is `Node` and some of its modules but even that is planned to be removed in the future.

Host: NoGapDef.defHost(function(SharedTools, Shared, SharedContext) {
var iAttempt = 0;
var nBytes = 0;
return {
Public: {
tellClientSomething: function(sender) {
this.client.showHostMessage('We have exchanged ' + ++iAttempt + ' messages.');
tellMeSomething: function(message) {
nBytes += (message && message.length) || 0;
this.client.showHostMessage('Host has received a total of ' + nBytes + ' bytes.');
}

@@ -141,10 +141,12 @@ }

initClient: function() {
window.clickMe = function() {
document.body.innerHTML +='Button was clicked.<br />';
this.host.tellClientSomething();
}.bind(this);
// bind a button to a component function (quick + dirty):
window.clickMe = this.onButtonClick.bind(this);
document.body.innerHTML += '<button onclick="window.clickMe();">Click Me!</button><br />';
},
onButtonClick: function() {
document.body.innerHTML +='Button was clicked.<br />';
this.host.tellMeSomething('hello!');
},
Public: {

@@ -169,3 +171,3 @@ showHostMessage: function(msg) {

* `this.host` gives us an object on which we can call `Public` methods on the host
* For example, we can call `tellClientSomething` which is a method that was defined in `Host.Public`
* For example, we can call `tellMeSomething` which is a method that was defined in `Host.Public`
* Once the host receives our request, it calls `this.client.showHostMessage`

@@ -175,28 +177,80 @@ * Note: `this.host` (available on Client) vs. `this.client` (available on Host)

## Full-stack promise chains
<!-- [Link](samples/). -->
NoGap supports full-stack [Promise chains](https://github.com/petkaantonov/bluebird#what-are-promises-and-why-should-i-use-them). Meaning you can let the Client wait until a Host-side function call has returned. And you can even return a value from a Host function, and it will arrive at the Client. Errors also traverse the entire stack!
Code snippet:
```js
tellMeSomething: function(name) {
nBytes += (message && message.length) || 0;
return 'Host has received a total of ' + nBytes + ' bytes.';
}
// ...
onButtonClick: function() {
document.body.innerHTML +='Button was clicked.<br />';
this.host.tellMeSomething('hello!')
.bind(this) // this is tricky!
.then(function(hostMessage) {
this.showHostMessage(hostMessage);
});
},
```
**New Concepts**
* Calling a `Public` function on a component's `host` object returns a promise.
* That promise is part of a full-stack [Promise chains](https://github.com/petkaantonov/bluebird#what-are-promises-and-why-should-i-use-them). A value returned by a `Host`'s `Public` function (or by a promise returned by such function), will be received by the client.
* Note that [JavaScript's `this` is tricky](http://javascriptissexy.com/understand-javascripts-this-with-clarity-and-master-it/)!
## TwoWayStreetAsync
[Link](samples/TwoWayStreetAsync).
Now that our code keeps growing and you are starting to get the picture, let us just focus on code snippets from now on.
Imagine the server had to do an [asynchronous operation](http://msdn.microsoft.com/en-us/library/windows/apps/hh700330.aspx) in [`tellMeSomething`](#twowaystreet), such as reading a file, or getting something from the database.
Imagine the server had to do an asynchronous operation in [`tellClientSomething`](#twowaystreet).
For example, it needs to read a file, or get something from the database.
We can simply use promises for that!
```js
tellClientSomething: function() {
this.Tools.keepOpen();
// wait 500 milliseconds before replying
setTimeout(function() {
tellMeSomething: function() {
Promise.delay(500) // wait 500 milliseconds before replying
.bind(this) // this is tricky!
.then(function() {
this.client.showHostMessage('We have exchanged ' + ++iAttempt + ' messages.');
this.Tools.flush();
}.bind(this), 500);
});
}
```
And again, we can just return the message and it will arrive at the Client automagically, like so:
```js
tellMeSomething: function() {
Promise.delay(500) // wait 500 milliseconds before replying
.bind(this) // this is tricky!
.then(function() {
return 'We have exchanged ' + ++iAttempt + ' messages.';
});
}
// ...
onButtonClick: function() {
document.body.innerHTML +='Button was clicked.<br />';
this.host.tellMeSomething()
.bind(this) // this is tricky!
.then(function(hostMessage) {
this.showHostMessage(hostMessage);
});
},
```
**New Concepts**
* We need to perform an asynchronous request whose result is to be sent to the other side:
* In that case, first call `this.Tools.keepOpen()`, so the client connection will not be closed automatically
* Once you sent everything to the client, call `this.Tools.flush()`
* We need to perform an asynchronous request whose result is to be sent to the other side
* Simply use [Promise chains](https://github.com/petkaantonov/bluebird#what-are-promises-and-why-should-i-use-them)!
## CodeSharingValidation

@@ -218,3 +272,3 @@ [Link](samples/CodeSharingValidation).

Public: {
setValue: function(sender, value) {
setValue: function(value) {
this.value = this.Shared.validateText(value);

@@ -287,3 +341,2 @@ // ...

## Multiple Components

@@ -298,3 +351,10 @@

## Full-stack error handling
TODO!
* Feel free to try and throw an error or use `Promise.reject` in a `Host`'s `Public` function, and then `catch` it on the Client side. You will notice that, for security reasons, the contents of Host-side exceptions are modified before being sent to the Client.
* You can override `Tools.onError` to customize error handling (especially on the server)
* TODO: Trace verbosity configuration
## Dynamic Loading of Components

@@ -310,40 +370,2 @@ <!-- [Link](samples/DynamicLoading). -->

## Request &lt;-> Reply Pairs
<!-- [Link](samples/). -->
Code snippet:
Host: {
Public: {
myStuff: [...],
checkIn: function(sender, name) {
// call Client's `onReply` callback
sender.reply('Thank you, ' + name + '!', myStuff);
}
}
}
// ...
Client: {
// ...
initClient: {
// call function on Host, then wait for Host to reply
this.host.checkIn('Average Joe')
.onReply(function(message, stuff) {
// server sent something back
// ...
});
}
}
**Concepts**
* When calling a `Host.Public` method (e.g. `checkIn`), in addition to the arguments sent by the client, there is an argument injected before all the others, called `sender`.
* When calling a `Host.Public` method, you can register a callback by calling `onReply` (e.g. `checkIn(...).onReply(function(...) { ... }`).
* The `Host` can then call `sender.reply` which will lead to the `onReply` callback to be called.
## Simple Sample App

@@ -367,13 +389,13 @@ [Link](samples/sample_app).

1. The **shared object** of a component exists only once for the entire application. It is what is returned if you `require` the component file in Node. You can access all of shared component objects through the `Shared` set which is the second argument of every `Host`'s *component definition*.
1. The **Shared object** of a component is a singleton; it exists only once for the entire application. You can access all `Shared` component objects through the `Shared` set which is the second argument of every `Host`'s *component definition*.
2. The **instance object** of a component exists once for every client. Every client that connects to the server, gets its own set of instances of every active component. On the `Host` side, the *instance object* of a component is defined as the merged result of all members of `Private` and `Public` which we call *instance members*. These instance members are accessible through `this.Instance` from **instance code**, that is code inside of `Private` and `Public` properties. If you want to hook into client connection and component bootstrapping events, simply defined `onNewClient` or `onClientBootstrap` functions inside `Host.Private`. You can access the respective *shared members* through `this.Shared` from *instance code*.
Inside a `Host` instance object, you can directly call `Public` instance members on the client through `this.client.someClientPublicMethod(some, data)`. Being able to directly call a function on a different computer or in a different program is called [RPC (Remote Procedure Calls)](http://en.wikipedia.org/wiki/Remote_procedure_call). Similarly, `Client` instances can directly call `this.host.someHostPublicMethod`. Note that when you call `Host.Public` methods, an argument gets injected before all other arguments, called the `sender`. The `sender` argument gives context sensitive information on where the call originated from and can be used for simple request &lt;-> **reply** pairs, and for debugging purposes.
2. The **instance object** of a component exists once for every client. Every client that connects to the server, gets its own set of instances of every active component. On the `Host` side, the *instance object* of a component is defined as the merged result of all members of `Private` and `Public` which we call *instance members*. These instance members are accessible through `this.Instance` from **instance code**, that is, code inside of `Private` and `Public` properties. If you want to hook into client connection and component bootstrapping events, simply define `onNewClient` or `onClientBootstrap` functions inside `Host.Private`. You can access the owning component's *Shared singleton* through `this.Shared` from within `Private` or `Public` functions.
Inside a `Host` instance object, you can directly call `Public` instance members on the client through `this.client.someClientPublicMethod(some, data)`. Being able to directly call a function on a different computer or in a different program is called [RPC (Remote Procedure Call)](http://en.wikipedia.org/wiki/Remote_procedure_call). Similarly, `Client` instances can directly call `this.host.someHostPublicMethod` which returns a [Promise](https://github.com/petkaantonov/bluebird#what-are-promises-and-why-should-i-use-them) which will be fulfilled once the `Host` has run the function and notified the client.
## `Client`
The set of all `Client` endpoint definition is automatically sent to the client and installed, as soon a client connects. On the client side, `this.Shared` and `this.Instance` refer to the same object, and `Private` and `Public` are both merged into the `Client` *component definition* itself. If you want to load components dynamically (or lazily; `lazyLoad` is set to 1), during certain events, you need to set the `lazyLoad` config parameter to `true` or `1`.
The set of all `Client` endpoint definitions is automatically sent to the client and installed, as soon as a client connects. On the client side, `this.Shared` and `this.Instance` refer to the same object, and `Private` and `Public` are both merged into the `Client` *component definition* itself. If you want to load components dynamically (i.e. lazily), you need to set the `lazyLoad` config parameter to `true` or `1`.
## `Base`
Everything from the `Base` definition is merged into both, `Host` and `Client`. `Public` and `Private` are also merged correspondingly. Since `Host` and `Client` operate slightly different, certain naming decisions had to be made seemingly in favor of one over the other. E.g. the `Shared` concept does not exist on client side (because a `Client` only contains a single instance of all components), so there, it simply is the same as `Instance`.
Inside `Base` members, you can call `this.someMethod` even if `someMethod` is not declared in `Base`, but instead is declared in `Host` as well as `Client`. At the same time, you can call `this.someBaseMethod` from each endpoint definition. That enables you to easily have shared code call endpoint-specific code and vice versa, thereby supporting polymorphism and encapsulation.
Inside `Base` members, you can call `this.someMethod` even if `someMethod` is not declared in `Base`, but instead is declared in `Host` as well as `Client`. At the same time, you can call `this.someBaseMethod` from `Client` or `Host`. That enables you to easily have shared code call endpoint-specific code and vice versa, thereby supporting polymorphism and encapsulation.

@@ -383,3 +405,3 @@

<a name="component_skeleton"></a>
This skeleton code summarizes (most of) available component structure:
This skeleton code summarizes (most of) the available component structure:

@@ -450,3 +472,3 @@

* The ctor is called only once, during NoGap initialization,
* when the shared component part is created.
* when the `Shared` component part is created.
* Will be removed once called.

@@ -459,3 +481,3 @@ */

* Is called once on each component after
* all components have been created.
* all components have been created, and after `initBase`.
*/

@@ -485,4 +507,4 @@ initHost: function() {

* Called after `onNewClient`, once this component
* is bootstrapped on the client side.
* Since components can be deployed dynamically,
* is about to be sent to the `Client`.
* Since components can be deployed dynamically (if `lazyLoad` is enabled),
* this might happen much later, or never.

@@ -516,4 +538,3 @@ */

* Called once after all currently deployed client-side
* components have been created.
* Will be removed once called.
* components have been created, and after `initBase`.
*/

@@ -525,4 +546,5 @@ initClient: function() {

/**
* Called after the given component has been loaded in the client.
* NOTE: This is important when components are dynamically loaded (`lazyLoad` = 1).
* Called after the given component has been loaded in the Client.
* NOTE: This is generally only important when components are dynamically loaded (`lazyLoad` = 1).
* (Because else, `initClient` will do the trick.)
*/

@@ -534,6 +556,7 @@ onNewComponent: function(newComponent) {

/**
* Called after the given batch of components has been loaded in the client.
* Called after the given batch of components has been loaded in the Client.
* This is called after `onNewComponent` has been called
* on each individual component.
* NOTE: This is important when components are dynamically loaded (`lazyLoad` = 1).
* NOTE: This is generally only important when components are dynamically loaded (`lazyLoad` = 1).
* (Because else, `initClient` will do the trick.)
*/

@@ -545,4 +568,4 @@ onNewComponents: function(newComponents) {

/**
* This is optional and will be merged into the Client instance,
* residing along-side the members defined above.
* This will be merged into the Client instance.
* It's members will reside along-side the members defined above it.
*/

@@ -567,2 +590,4 @@ Private: {

TODO: Need to rewrite this with to work with the new version that adapted full-stack Promises.
This tutorial is aimed at those who are new to `NoGap`, and new to `Node` in general.

@@ -647,6 +672,3 @@ It should help you bridge the gap from the [Code Snippets](#samples) to a real-world application.

* If you are interested into the dirty details, have a look at [`HttpPostImpl` in `ComponentCommunications.js`](https://github.com/Domiii/NoGap/blob/master/lib/ComponentCommunications.js#L564)
* `traceKeepOpen` (Default = 0)
* This is for debugging your `keepOpen` and `flush` pairs. If you don't pair them up correctly, the client might wait forever.
* If your client does not receive any data, try setting this value to 4 and check if all calls pair up correctly.
* The value determines how many lines of stacktrace to show, relative to the first non-internal call; that is the first stackframe whose code is not located in the NoGap folder.
* TODO: Tracing, logging + customized error handling

@@ -696,3 +718,3 @@

=============
By default, each `Client` only receives code from `Client` and `Base` definitions. `Host`-only code is not available to the client. However, the names of absolute file paths are sent to the client to facilitate perfect debugging; i.e. all stacktraces and the debugger will refer to the correct line inside the actual host-resident component file. If that is of concern to you, let me know, and I'll move up TODO priority of name scrambling, or have a look at [`ComponentDef`'s `FactoryDef`, and the corresponding `def*` methods](https://github.com/Domiii/NoGap/blob/master/lib/ComponentDef.js#L71) yourself.
By default, each `Client` only receives `Client` and `Base` definitions. `Host`-only code is not available to the client. However, the names of absolute file paths are sent to the client to facilitate perfect debugging; i.e. all stacktraces and the debugger will refer to the correct line inside the actual host-resident component file. If that is of concern to you, let me know, and I'll move up TODO priority of name scrambling, or have a look at [`ComponentDef`'s `FactoryDef`, and the corresponding `def*` methods](https://github.com/Domiii/NoGap/blob/master/lib/ComponentDef.js#L71) yourself.

@@ -702,3 +724,3 @@

=============
TODO: Add more links + terms.
TODO: Add links + more terms.

@@ -708,9 +730,8 @@ * Component

* Client
* Base (mergd into Client and Host)
* Instance (set of all component instance objects)
* Shared (set of all component shared objects)
* Endpoint (refers to Client or Host)
* Base (merged into Client and Host)
* Shared (set of all component singletons)
* Instance (set of all component instance objects, exist each once per connected client)
* Tools (set of functions to assist managing of components)
* Context
* Asset (an asset is content data, such as html and css code, images and more)
* Asset (an asset is content data, such as HTML and CSS code, images and more)
* more...

@@ -722,2 +743,2 @@

Good luck! In case of questions, feel free to contact me.
Good luck! In case of any questions, feel free to contact me.

@@ -12,3 +12,3 @@

* Each such "command method" does not actually execute a command;
* instead, it sends a command-execution request to the other side via the underlying ComponentEndpointImpl.
* instead, it sends a command-execution request to the other side via the underlying ComponentTransportImpl.
*/

@@ -23,6 +23,7 @@ var CommandProxy = ComponentDef.lib({

// class for every host-side command proxy (the `client` object of each component instance)
var Client,
Sender;
var ClientProxy;
var HostDef;
var Promise;
var addClientCommandProxy = function(componentInstance) {
var addClientProxy = function(componentInstance) {
// get all public commands

@@ -33,11 +34,17 @@ var def = componentInstance.Shared._def;

var client = componentInstance.client = new Client(componentInstance);
var client = componentInstance.client = new ClientProxy(componentInstance);
// add all commands to the `client` object
toCommandNames.forEach(function(cmdName) {
client[cmdName] = function(argsssssssss) {
// add all commands to the `ClientProxy`
toCommandNames.forEach(function(methodName) {
client[methodName] = function(argsssssssss) {
// client.someCommand(...) lands here:
var compName = this._componentInstance.Shared._def.FullName;
var args = Array.prototype.slice.call(arguments, 0); // convert arguments to array
componentInstance.Instance.Libs.ComponentCommunications.sendCommandToClient(compName, cmdName, args);
componentInstance.Tools.traceComponentFunctionCall(compName + '.client', methodName, args);
// append this proxied command to buffer
// which will be sent to Client when current (or next) Client response is sent back
var hostResponse = componentInstance.Instance.Libs.ComponentCommunications.hostResponse;
hostResponse.bufferCommand(compName, methodName, args);
}.bind(client);

@@ -47,46 +54,14 @@ }.bind(this));

var HostDef;
return HostDef = {
__ctor: function() {
Client = squishy.createClass(
Promise = Shared.Libs.ComponentDef.Promise;
ClientProxy = squishy.createClass(
function(componentInstance) {
this._componentInstance = componentInstance;
},{
logError: function(msg) {
console.error(this + ' - ' + msg);
},
logWarn: function(msg) {
console.warn(this + ' - ' + msg);
},
log: function(msg) {
console.log(this + ' - ' + msg);
},
},{
toString: function() {
// TODO: Better string representation
return 'Client';
return 'ClientProxy';
}
});
Sender = {
reply: function() {
if (!this.replyId) {
console.warn(new Error(
'Host called `sender.reply` when there was no pending request expecting a reply from component `' +
this._componentInstance.Shared._def.FullName + '`.').stack);
return;
}
var args = Array.prototype.slice.call(arguments, 0); // convert arguments to array
this._componentInstance.Instance.Libs.ComponentCommunications.sendReply(this.replyId, args);
},
toString: function() {
// TODO: Better string representation
return 'Sender';
}
};
},

@@ -101,12 +76,2 @@

Private: {
startCommandExecution: function() {
// connection management
this.Instance.Libs.ComponentCommunications.onStartCommandExecution();
},
finishCommandExecution: function() {
// connection management
this.Instance.Libs.ComponentCommunications.onFinishCommandExecution();
},
/**

@@ -118,3 +83,3 @@ * Called by ComponentBootstrap to create and attach a `client` property to each component instance.

this.Instance.forEachComponentOfAnyType(function(componentInstance) {
addClientCommandProxy(componentInstance);
addClientProxy(componentInstance);
}.bind(this));

@@ -124,5 +89,8 @@ },

/**
* This is called when the client sent some commands to the host: Iterate and execute them.
* This is called when the Client sent some commands to the Host:
* Iterate and execute them in parallel.
* @return A promise that will deliver an array of `commandExecutionResults`,
* each corresponding to its respective entry in the `allCommands` array
*/
executeClientCommandsNow: function(allCommands) {
executeClientCommands: function(allCommands) {
if (allCommands.length > maxCommandsPerRequestDefault) {

@@ -137,38 +105,36 @@ // TODO: This is to prevent basic command flooding, but needs improvement.

// iterate over all given commands
for (var i = 0; i < allCommands.length; ++i) {
var command = allCommands[i];
return Promise.map(allCommands, function(command) {
var componentName = command.comp;
var commandName = command.cmd;
var methodName = command.cmd;
var args = command.args;
// TODO: More type- and other sanity checks!
var componentInstance = this.Instance.getComponentOfAnyType(componentName);
if (!componentInstance) {
// TODO: Proper logging & consequential actions?
componentInstance.client.logWarn('Client sent invalid command for component: ' + componentName);
break;
this.Tools.logWarn('sent invalid command for component: ' + componentName);
}
// make sure, command is not mal-formatted
else if (args && !(args instanceof Array)) {
this.Tools.logWarn('sent invalid command args for command: ' +
componentInstance.Shared._def.getFullInstanceMemberName('Public', methodName));
}
// get arguments and command function
if (!componentInstance.Shared._def.Public[commandName] || !componentInstance[commandName]) {
// TODO: Proper logging & consequential actions?
componentInstance.client.logWarn('Client sent invalid command: ' +
componentInstance.Shared._def.getFullInstanceMemberName('Public', commandName));
break;
else if (!componentInstance.Shared._def.Public[methodName] || !componentInstance[methodName]) {
this.Tools.logWarn('sent invalid command: ' +
componentInstance.Shared._def.getFullInstanceMemberName('Public', methodName));
}
if (args && !(args instanceof Array)) {
componentInstance.client.logWarn('Client sent invalid command args for command: ' +
componentInstance.Shared._def.getFullInstanceMemberName('Public', commandName));
break;
else {
// call actual command, with arguments on the host object
this.Tools.traceComponentFunctionCallFromSource('Client', componentName, methodName, args);
return this.Instance.Libs.ComponentCommunications.executeUserCodeAndSerializeResult(function() {
return Promise.resolve()
.then(function() {
return componentInstance[methodName].apply(componentInstance, args);
});
}.bind(this));
}
// inject sender as first argument
var sender = Object.create(Sender);
sender.replyId = command.replyId;
sender._componentInstance = componentInstance;
args.splice(0, 0, sender);
// call actual command, with arguments on the host object
componentInstance[commandName].apply(componentInstance, args);
}
}.bind(this));
},

@@ -214,8 +180,13 @@ }

// create proxy method for each command
toCommandNames.forEach(function(cmdName) {
host[cmdName] = function(argsssssss) {
toCommandNames.forEach(function(methodName) {
host[methodName] = function(argsssssss) {
// this.host.someCommand(...) lands here:
var args = Array.prototype.slice.call(arguments, 0); // convert arguments to array
var componentName = componentInstance._def.FullName;
return Instance.Libs.ComponentCommunications.sendCommandToHost(componentInstance._def.FullName, cmdName, args);
// return promise of return value
this.Tools.traceComponentFunctionCall(componentName + '.host', methodName, args);
return Instance.Libs.ComponentCommunications.sendCommandToHost(
componentName, methodName, args)
.bind(componentInstance); // bind host promises to host proxy's own instance by default
}.bind(this);

@@ -229,3 +200,3 @@ }.bind(this));

*/
execHostCommands: function(allCommands) {
executeHostCommands: function(allCommands) {
// iterate over all commands

@@ -235,13 +206,15 @@ for (var i = 0; i < allCommands.length; ++i) {

var componentName = command.comp;
var commandName = command.cmd;
var methodName = command.cmd;
var args = command.args;
Tools.traceComponentFunctionCallFromSource('Host', componentName, methodName, args);
var componentInstance = Instance.getComponentOfAnyType(componentName);
if (!componentInstance) {
console.error('Host sent command for invalid component: ' + componentName + ' (' + commandName + ')');
console.error('Host sent command for invalid component: ' + componentName + ' (' + methodName + ')');
continue;
}
if (!componentInstance._def.Public[commandName]) {
console.error('Host sent invalid command: ' + componentInstance._def.getFullInstanceMemberName('Public', commandName));
if (!componentInstance._def.Public[methodName]) {
console.error('Host sent invalid command: ' + componentInstance._def.getFullInstanceMemberName('Public', methodName));
continue;

@@ -251,3 +224,3 @@ }

// call command
componentInstance[commandName].apply(componentInstance, args);
componentInstance[methodName].apply(componentInstance, args);
}

@@ -254,0 +227,0 @@ },

@@ -64,2 +64,8 @@ /**

return {
NoGapIncludes: {
js: [
'bluebird.min.js'
]
},
getPublicUrl: function(cfg) {

@@ -105,3 +111,3 @@ var publicUrl = url.resolve(baseUrl, cfg.publicPath);

*/
forEachAsset: function(category, cb, componentsOrNames) {
forEachAsset: function(category, cb, componentsOrNames, arg) {
var iterator = function(component) {

@@ -111,3 +117,3 @@ if (component.Assets && component.Assets[category]) {

var entry = component.Assets[category];
cb(component, entry);
cb(component, entry, arg);
}

@@ -183,8 +189,8 @@ catch (err) {

var iterator = function(component, files) {
var generateAssetIncludeCode = function(component, autoIncludes, folder) {
var compName = component._def.FullName;
var compFolder = component._def.Folder;
folder = folder || component._def.Folder;
for (var categoryName in files) {
if (!files.hasOwnProperty(categoryName)) continue;
for (var categoryName in autoIncludes) {
if (!autoIncludes.hasOwnProperty(categoryName)) continue;
var allFilesInCat = allFilesPerCat[categoryName];

@@ -204,3 +210,3 @@ if (!allFilesInCat) {

var filesInCat = files[categoryName];
var filesInCat = autoIncludes[categoryName];
for (var i = 0; i < filesInCat.length; ++i) {

@@ -225,13 +231,14 @@ var fname = filesInCat[i];

// local URL
var fpath = path.join(compFolder, fname);
var actualPath = fpath.split('?', 1)[0];
if (!fs.existsSync(actualPath)) {
var fpath = path.join(folder, fname);
var path1 = fpath.split('?', 1)[0];
if (!fs.existsSync(path1)) {
// file does not exist relative to component:
// check if file exists in public directory
fpath = path.join(pubFolderAbs, fname);
actualPath = fpath.split('?', 1)[0];
if (!fs.existsSync(actualPath)) {
var path2 = fpath.split('?', 1)[0];
if (!fs.existsSync(path2)) {
// cannot find file
throw new Error('Could not find file `' + actualPath + '` for component `' + component +
'`. It was located neither relative to the component, nor in the public folder.');
throw new Error('Could not find file `' + fname + '` for component `' + component +
'`. It was located neither relative to the component, nor in the public folder (' +
[folder, pubFolderAbs] + ').');
}

@@ -247,4 +254,10 @@ }

}.bind(this);
this.forEachAsset('AutoIncludes', iterator);
// add NoGap assets
var nogapAssetFolder = __dirname + '/../assets/';
generateAssetIncludeCode(this, this.NoGapIncludes, nogapAssetFolder);
// add all other assets
this.forEachAsset('AutoIncludes', generateAssetIncludeCode);
return includeCode;

@@ -334,3 +347,2 @@ },

// TODO: Support categories
// TODO: JSON.stringify & parse
allFilesInCat[compPath] = data = fs.readFileSync(fpath).toString('utf8');

@@ -337,0 +349,0 @@ }

/**
* ComponentBootstrap is responsible for bootstrapping client-side component code.
* The bootstrapping implementation (which needs to be selected upon start-up) determines how the components are bootstrapped on the client.
*/

@@ -14,43 +13,2 @@ "use strict";

/**
* Interface of a bootstrapper implementation.
*
* @interface
*/
var BootstrapperImpl = {
ImplName: "<short name describing implementation type>",
Base: ComponentDef.defBase(function(SharedTools, Shared, SharedContext) { return {
assetHandlers: {
autoIncludeResolvers: {},
autoIncludeCodeFactories: {}
}
};}),
Host: ComponentDef.defHost(function(SharedTools, Shared, SharedContext) { return {
setGlobal: function(varName, varValue) {},
/**
* Actually bootstrap the NoGap library.
* Called by `ComponentLoader.start`.
*/
bootstrap: function(app, cfg) {}
};}),
Client: ComponentDef.defClient(function(Tools, Instance, Context) {
return {
setGlobal: function(varName, varValue) {},
Public: {
/**
* Do it all over: Kill current instance and try again.
* This is effectively a page refresh for browsers.
* For webworkers or other environments, this needs to be done very differently.
*/
refresh: function() { }
}
};
})
};
/**
* Defines a registry for component bootstrapping methods.

@@ -60,18 +18,3 @@ */

Base: ComponentDef.defBase(function(Shared) { return {
getImplComponentLibName: function(name) {
//return 'ComponentBootstrapImpl_' + name;
return 'ComponentBootstrapImpl_';
},
getCurrentBootstrapperImpl: function() {
return Shared.Libs[this.getImplComponentLibName()];
},
Private: {
/**
* Get the current instance of the bootstrapper implementation.
*/
getCurrentBootstrapperImpl: function() {
return this.Instance.Libs[this.Shared.getImplComponentLibName()];
},
}

@@ -81,2 +24,3 @@ }}),

Host: ComponentDef.defHost(function(SharedTools, Shared, SharedContext) {
var Promise;
return {

@@ -87,36 +31,12 @@ // ###############################################################################################################################

__ctor: function() {
this.implementations = {};
Promise = Shared.Libs.ComponentDef.Promise;
},
/**
* Register a custom bootstrapper implementation.
*/
registerBootstrapper: function(implType) {
squishy.assert(typeof(implType) === 'object',
'BootstrapperImpl definition must implement the `BootstrapperImpl` interface.');
squishy.assert(implType.ImplName, 'Unnamed BootstrapperImpl is illegal. Make sure to set the `ImplName` property.');
// register implementation as component, so we get it's client side functionality as well
implType.Name = this.getImplComponentLibName();
// register implementation as lib (so we also get access to it on the client side), and store it in implementations array.
this.implementations[implType.ImplName] = ComponentDef.lib(implType);
},
/**
* Get the bootstrapper implementation of the given name.
*/
getBootstrapper: function(name) {
var booter = this.implementations[name];
squishy.assert(booter, 'Invalid bootstrapper: ' + name + ' - Possible choices are: ' + Object.keys(this.implementations));
return booter;
},
/**
* This magic function kicks everything off.
* It's called right after all components have been installed.
*/
bootstrap: function(app, cfg) {
// get bootstrapper implementation
var bootstrapperImpl = this.getBootstrapper(cfg.bootstrapper || 'HttpGet');
var bootstrapperImpl = Shared.Libs.ComponentCommunications.getComponentTransportImpl();

@@ -127,140 +47,76 @@ // kick it!

/**
* Sets the charset for all bootstrapping operations.
* The default bootstrapper bootstraps NoGap to the browser and
* uses this charset to populate the corresponding META tag.
*/
setCharset: function(charset) {
this.charset = charset;
},
Private: {
// ###############################################################################################################################
// Core bootstrapping routines
// ###############################################################################################################################
// Tools for bootstrapper implementations
/**
* Installs component instance and returns the code to setup the Component system.
*/
bootstrapComponentInstance: function(clientAddr, clientRoot) {
console.assert(clientRoot, 'clientRoot must be provided for component installation.');
/**
* Installs component instance and returns the code to setup the Component system.
*/
installComponentInstanceAndGetClientBootstrapCode: function(session, sessionId, clientAddr, clientRoot, cb) {
console.assert(clientRoot, 'clientRoot must be provided for component installation.');
// Move through the communications queue.
// This will make sure that parallel requests (even with asynchronous results) are serialized.
// This reduces complexity and makes it more difficult for clients to spam.
return this.Instance.Libs.ComponentCommunications.executeInOrder(function() {
// set bootstrapping flag
this.isBootstrapping = true;
// get or create Instance map
var Instance = Shared.Libs.ComponentInstance.createInstanceMap(sessionId);
// store address & port, so all client implementations know where to connect to for commands.
this.Instance.Libs.ComponentContext.touch(); // update last used time
// Move through the communications queue.
// This will make sure that parallel requests (even with asynchronous results) are serialized.
// This is necessary to make the `keepOpen` + `flush` pairing work properly.
// This reduces complexity and makes it more difficult for clients to spam.
// But of course this also slows things down, if the application protocol
// requires a lot of work that can be executed in parallel
// and is not sent in batch requests.
Instance.Libs.ComponentCommunications.executeInOrder(function(moveNext) {
// set session
Instance.Libs.ComponentSession.setSession(session, sessionId);
// store some client information
this.Context.clientAddr = clientAddr;
this.Context.clientIsLocal = clientAddr === 'localhost' || clientAddr === '127.0.0.1';
this.Context.clientRoot = clientRoot;
// get instance of this
var thisInstance = Instance.Libs.ComponentBootstrap;
// initialize host-side installation and collect code for user-specific initialization commands:
var allClientDefinitions = Shared.Libs.ComponentDef.getAllClientDefinitions();
var libNames = allClientDefinitions.Libs.names;
var otherComponentNames = allClientDefinitions.Components.names;
// set bootstrapping flag
thisInstance.isBootstrapping = true;
// run initialization code
return Promise.resolve()
// store address & port, so all client implementations know where to connect to for commands.
Instance.Libs.ComponentContext.touch(); // update last used time
// call `onNewClient` on all libs
.then(this.callComponentMethods.bind(this, libNames, 'onNewClient'))
// store some client information
thisInstance.Context.clientAddr = clientAddr;
thisInstance.Context.clientIsLocal = clientAddr === 'localhost' || clientAddr === '127.0.0.1';
thisInstance.Context.clientRoot = clientRoot;
// call `onNewClient` on all other components
.then(this.callComponentMethods.bind(this, otherComponentNames, 'onNewClient'))
// initialize host-side installation and collect code for user-specific initialization commands:
var libNames = Shared.Libs.ComponentDef.clientDefs.Libs.names;
var componentNames = Shared.Libs.ComponentDef.clientDefs.Components.names;
// call `onClientBootstrap` on all libs
.then(this.callComponentMethods.bind(this, libNames, 'onClientBootstrap'))
}.bind(this))
// store all commands to be executed on the client side
var libCmds = [];
var componentCmds = [];
.bind(this)
// call `onNewClient` on all libs
var step1 = function() {
thisInstance.collectCommandsFromHostCalls(libNames, libCmds, 'onNewClient', step2);
};
.then(function(bootstrapHostResponseData) {
//this.Tools.traceLog('Sending installer code...');
// now, build complete NoGap installation code and return to caller
var code = ComponentDef._buildClientInstallCode(bootstrapHostResponseData);
// call `onClientBootstrap` on all libs
var step2 = function() {
thisInstance.collectCommandsFromHostCalls(libNames, libCmds, 'onClientBootstrap', step3);
};
// give code back to caller
return code;
})
.finally(function() {
// we finished Host-side bootstrapping
this.isBootstrapping = false;
});
},
// call `onNewClient` on all other components
var step3 = function() {
thisInstance.collectCommandsFromHostCalls(componentNames, componentCmds, 'onNewClient',
function() {
// get code to initialize the component framework
var bootstrapData = {
libCmds: libCmds,
componentCmds: componentCmds
};
var code = ComponentDef.getClientInstallCode(bootstrapData);
// we are done with bootstrapping
thisInstance.isBootstrapping = false;
// give code back to caller and bootstrap client
cb(code);
// move next in queue
moveNext();
});
};
// go!
step1();
});
},
Private: {
/**
* Get client-specific initialization code
* by calling the given method on all of the given component instances,
* while buffering all commands to be executed on client side and copying them to `allCommands`.
* Calls `cb`, once finished.
* Call the given method on all of the given component instances.
*/
collectCommandsFromHostCalls: function(componentNames, allCommands, methodName, doneCb) {
callComponentMethods: function(componentNames, methodName) {
var This = this;
var Instance = this.Instance;
// call the given method on every component that has it
var callMethods = function() {
componentNames.forEach(function(componentName) {
var component = Instance.getComponentOfAnyType(componentName);
if (component[methodName]) {
component[methodName]();
}
});
};
// collect all commands raised by the given components
if (this.isBootstrapping) {
// use connection overrides to get all commands to be sent
var connection = {
sendCommandsToClient: function(initCommands) {
// store all commands
for (var i = 0; i < initCommands.length; ++i) {
allCommands.push(initCommands[i]);
}
},
staysOpen: function() { return false; }
};
Instance.Libs.ComponentCommunications.executeCommandRaisingCode(connection,
// call all methods, while all resulting commands are buffered
callMethods,
// notify caller after we are done intercepting
doneCb);
}
else {
// the commands to be raised by the calls can just be carried over by the default connection implementation
callMethods();
doneCb();
}
// Run commands right away (do not queue), then give results back to caller
return Promise.map(componentNames, function(componentName) {
var component = This.Instance.getComponentOfAnyType(componentName);
if (component[methodName] instanceof Function) {
// execute application code
This.Tools.traceComponentFunctionCall(componentName, methodName);
return component[methodName]();
}
});
},

@@ -277,24 +133,27 @@

// get definitions
var defs = Shared.Libs.ComponentDef.getClientComponentDefsForDeployment(componentNames);
var defs = Shared.Libs.ComponentDef._getClientComponentDefsForDeployment(componentNames);
// get assets
var bootstrapImplClient = Shared.Libs[this.ComponentBootstrap.getImplComponentLibName()];
console.assert(bootstrapImplClient, 'Could not lookup the host endpoint of the bootstrapper implementation.');
var clientAssets = Shared.Libs.ComponentAssets.getClientAssets(componentNames, bootstrapImplClient.assetHandlers);
var transportImpl = Shared.Libs.ComponentCommunications.getComponentTransportImpl();
// send component definitions and assets to Client, and bootstrap them
var clientAssets = Shared.Libs.ComponentAssets.getClientAssets(componentNames, transportImpl.assetHandlers);
this.client.bootstrapNewComponents(defs, clientAssets);
// call `onClientBootstrap` for new components on Host
componentNames.forEach(function(componentName) {
return Promise.resolve(componentNames)
.bind(this)
.map(function(componentName) {
var component = this.Instance.getComponentOfAnyType(componentName);
if (!component) {
This.client.logError('Tried to bootstrap invalid component: ' + component);
return;
return Promise.reject(new Error('Tried to send invalid component to Client: ' + componentName));
}
if (component.onClientBootstrap) {
component.onClientBootstrap();
}
}.bind(this));
})
// all components are available!
.then(function() {
// call `onClientBootstrap` on all new components
return this.callComponentMethods(componentNames, 'onClientBootstrap');
});
// TODO: Some better dependency management

@@ -316,10 +175,2 @@ // // add `explicitly requested` components

// install new components + assets, & fire events
},
requestClientComponentsFromHost: function(componentNames) {
this.requestClientComponents(null, componentNames);
},
refresh: function() {
this.getCurrentBootstrapperImpl().client.refresh();
}

@@ -332,6 +183,5 @@ },

*/
requestClientComponents: function(sender, componentNames) {
requestClientComponents: function(componentNames) {
if (!componentNames || !componentNames.length) {
this.client.logWarn('Insufficient arguments for `requestClientComponents`.');
return;
return Promise.reject('Insufficient arguments for `requestClientComponents`.');
}

@@ -347,4 +197,3 @@

' - Available components are: ' + Object.keys(this.Instance);
this.client.logWarn(err);
return;
return Promise.reject(err);
}

@@ -358,4 +207,3 @@ // else if (clientComponents[compName]) {

var err = 'Called `requestClientComponents` but `mayClientRequestComponent` returned false: ' + compName;
this.client.logWarn(err);
return;
return Promise.reject(err);
}

@@ -365,3 +213,3 @@ }

// actually enable the components
this.bootstrapNewClientComponents(componentNames);
return this.bootstrapNewClientComponents(componentNames);
}

@@ -373,17 +221,2 @@ }

Client: ComponentDef.defClient(function(Tools, Instance, Context) {
var pendingInitializers = [];
var addInitializerCallback = function(names, cb) {
pendingInitializers.push({
names: names,
cb: cb
});
};
/**
* Names of components already requested but not there yet.
*/
var pendingComponents = {},
nPendingComponents = 0;
var thisInstance;

@@ -395,12 +228,7 @@ return thisInstance = {

*/
onClientReady: function(bootstrapData) {
onClientReady: function(bootstrapHostResponseData) {
// execute initial commands on client
Instance.Libs.CommandProxy.execHostCommands(bootstrapData.libCmds);
Instance.Libs.CommandProxy.execHostCommands(bootstrapData.componentCmds);
Instance.Libs.ComponentCommunications.handleHostResponse(bootstrapHostResponseData);
},
refresh: function() {
this.getCurrentBootstrapperImpl().refresh();
},
/**

@@ -410,3 +238,3 @@ * Request the given set of new client components from the server.

*/
requestClientComponents: function(componentNames, cb) {
requestClientComponents: function(componentNames) {
console.assert(componentNames instanceof Array,

@@ -418,23 +246,15 @@ 'The first argument to `requestClientComponents` must be an array of component names.');

var compName = componentNames[i];
if (Instance[compName] || pendingComponents[compName]) {
if (Instance[compName]) {
// component already exists: remove
componentNames.splice(i, 1);
}
else {
++nPendingComponents;
pendingComponents[compName] = 1;
}
}
if (componentNames.length > 0) {
// remember cb, so it can be called when components have been enabled
addInitializerCallback(squishy.clone(componentNames), cb);
// send request to host
this.host.requestClientComponents(componentNames);
return this.host.requestClientComponents(componentNames);
}
else if (cb) {
// components are already ready, so we can fire right away
cb();
}
// always return a promise!
return Promise.resolve();
}

@@ -451,2 +271,3 @@ },

var componentName = componentDefs[i].Client.FullName;
componentDefs[i].toString = function() { return this.Client.FullName; };
if (Instance[componentName]) {

@@ -458,5 +279,4 @@ // ignore already existing components

}
--nPendingComponents;
delete pendingComponents[componentName];
}
Tools.traceLog('bootstrapping ' + componentDefs.length + ' new components: ' + componentDefs);

@@ -467,6 +287,7 @@ // install new components

// install the assets of the new components
var bootstrapImplClient = Instance.Libs[this.getImplComponentLibName()];
console.assert(bootstrapImplClient, 'Could not lookup BootstrapImpl client.');
Instance.Libs.ComponentAssets.initializeClientAssets(assets, bootstrapImplClient.assetHandlers);
var transportImpl = Instance.Libs.ComponentCommunications.getComponentTransportImpl();
console.assert(transportImpl, 'Could not lookup the host endpoint of the transport layer implementation.');
Instance.Libs.ComponentAssets.initializeClientAssets(assets, transportImpl.assetHandlers);
// do some more initialization and finally, call `initClient`

@@ -478,3 +299,4 @@ Instance.Libs.ComponentDef.initClientComponents(componentDefs, Instance);

if (component.onNewComponent) {
// call onNewComponent
// call onNewComponent many times
Tools.traceComponentFunctionCall(component.Shared._def.FullName, 'onNewComponent');
for (var j = 0; j < componentDefs.length; ++j) {

@@ -492,31 +314,6 @@ var componentName = componentDefs[j].Client.FullName;

};
Tools.traceComponentFunctionCall(component.Shared._def.FullName, 'onNewComponents');
component.onNewComponents(newComponents);
}
});
// we are done initializing, but there are some host-sent commands
// that are still pending that were sent in the same batch as this
// -> Defer until those commands have been executed
setTimeout(function() {
// check for pending `requestClientComponents` callbacks to call
for (var i = pendingInitializers.length-1; i >= 0; --i) {
var init = pendingInitializers[i];
var done = true;
for (var j = 0; j < init.names.length; ++j) {
if (!Instance[init.names[j]]) {
// this callback is still waiting for components that were not delivered
done = false;
break;
}
}
if (done) {
// all requested components have been loaded
// -> call callback & remove initializer
if (init.cb) {
init.cb();
}
pendingInitializers.splice(i, 1);
}
}
});
}

@@ -528,163 +325,2 @@ }

// ############################################################################################################
// Default bootstrapper implementations
/**
* Defines a set of default deployment methods (really, just one).
*/
var DefaultComponentBootstrappers = [
/**
* Simplest method: Component framework is deployed in Browser when navigating to some path.
*/
{
ImplName: 'HttpGet',
Base: ComponentDef.defBase(function(SharedTools, Shared, SharedContext) {
return {
/**
* Asset handlers are given to the Assets library for initializing assets.
*/
assetHandlers: {
/**
* Functions to fix asset filenames of given types.
*/
autoIncludeResolvers: {
js: function(fname) {
if (!fname.endsWith('.js')) fname += '.js';
return fname;
},
css: function(fname) {
if (!fname.endsWith('.css')) fname += '.css';
return fname;
}
},
/**
* Functions to generate code for including external file assets.
* Also need to fix tag brackets because this string will be part of a script
* that actually writes the asset code to the HTML document.
*
* @see http://stackoverflow.com/a/236106/2228771
*/
autoIncludeCodeFactories: {
js: function(fname) {
return '\x3Cscript type="text/javascript" src="' + fname + '">\x3C/script>';
},
css: function(fname) {
return '\x3Clink href="' + fname + '" rel="stylesheet" type="text/css">\x3C/link>';
},
/**
* Unsupported format: Provide the complete include string.
*/
raw: function(fname) {
return fname;
}
}
}
};
}),
Host: ComponentDef.defHost(function(SharedTools, Shared, SharedContext) {
return {
/**
* When the client connects, send it an an empty page with all the code
* necessary to deploy the component's client endpoint.
*/
bootstrap: function(app, cfg) {
// pre-build <script> & <link> includes
var includeCode = Shared.Libs.ComponentAssets.getAutoIncludeAssets(this.assetHandlers);
app.get(cfg.baseUrl + "*", function(req, res, next) {
// register error handler to avoid application crash
var onError = function(err) {
Shared.Libs.ComponentCommunications.reportConnectionError(req, res, err);
};
req.on('error', onError);
// This will currently cause bugs
// see: https://github.com/mikeal/request/issues/870
// req.socket.on('error', onError);
var session = req.session;
var sessionId = req.sessionID;
console.log('Incoming client requesting `' + req.url + '`');
console.assert(session,
'req.session was not set. Make sure to use a session manager before the components library, when using the default Get bootstrapping method.');
console.assert(sessionId,
'req.sessionID was not set. Make sure to use a compatible session manager before the components library, when using the default Get bootstrapping method.');
// get client root, so we know what address the client sees
var clientRoot = req.protocol + '://' + req.get('host');
var remoteAddr = req.connection.remoteAddress;
// install new instance and generate client-side code
var ComponentBootstrap = Shared.Libs.ComponentBootstrap;
ComponentBootstrap.installComponentInstanceAndGetClientBootstrapCode(
session, sessionId, remoteAddr, clientRoot, function(codeString, instanceContext) {
// determine charset
var charset = (ComponentBootstrap.charset || 'UTF-8');
// fix </script> tags in bootstrapping code
codeString = codeString.replace(/<\/script>/g, '\\x3c/script>');
// send out bootstrapping page to everyone who comes in:
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('<!doctype html>\n<html><head>');
// see: http://www.w3schools.com/tags/att_meta_charset.asp
res.write('<meta charset="' + charset + '" />');
// write CSS + JS external files
res.write(includeCode);
// done with head
res.write('</head><body>');
// write NoGap bootstrapping code
res.write('<script type="text/javascript" charset="' + charset + '">');
res.write('eval(eval(' + JSON.stringify(codeString) + '))');
res.write('</script>');
// wrap things up
res.write('</body></html>');
//console.log('Finished serving client: `' + req.url + '`');
res.end();
});
});
},
setGlobal: function(varName, varValue) {
GLOBAL[varName] = varValue;
}
};
}),
Client: ComponentDef.defClient(function(Tools, Instance, Context) {
return {
setGlobal: function(varName, varValue) {
window[varName] = varValue;
},
Public: {
refresh: function() {
console.log('Page refresh requested...');
window.location.reload();
}
}
};
})
}
];
DefaultComponentBootstrappers.forEach(function(impl) {
ComponentBootstrap.registerBootstrapper(impl);
});
module.exports = ComponentBootstrap;

@@ -9,2 +9,3 @@ /**

var ComponentDef = require('./ComponentDef');
var process = require('process');

@@ -16,19 +17,9 @@ /**

*/
var ComponentEndpointImpl = {
var ComponentTransportImpl = {
ImplName: "<short name describing implementation type>",
Host: ComponentDef.defHost(function(Tools, Shared) { return {
Host: ComponentDef.defHost(function(SharedTools, Shared, SharedContext) { return {
initHost: function(app, cfg) {},
Private: {
/**
* This host-side function tells the given client to execute the given command of the given component.
*/
sendCommandsToClient: function(commands, connectionState) {},
/**
* Whether this implementation is always open (like websockets),
* or whether it needs to explicitly requested to `keepOpen` and buffer command requests (like HttpPost).
*/
staysOpen: function() {}
}

@@ -46,3 +37,3 @@ }}),

*/
sendCommandsToHost: function(commands) {},
sendClientRequestToHost: function(clientRequest) {},
}

@@ -60,134 +51,138 @@ }})

var ComponentCommunications = ComponentDef.lib({
Base: ComponentDef.defBase(function(Tools, Shared) { return {
getImplComponentLibName: function(name) {
//return 'ComponentEndpointImpl_' + name;
return 'ComponentEndpointImpl';
},
Base: ComponentDef.defBase(function(SharedTools, Shared, SharedContext) { return {
/**
* PacketBuffer is used to keep track of all data while a
* request or response is being compiled.
* @see http://jsfiddle.net/h5Luk/15/
*/
PacketBuffer: squishy.createClass(function() {
this._resetBuffer();
}, {
// methods
createSingleCommandPacket: function(compName, cmdName, args) {
return [{
comp: compName,
cmd: cmdName,
args: args
}];
},
/**
* Buffers the given command request.
* Will be sent at the end of the current (or next) client request.
* @return Index of command in command buffer
*/
bufferCommand: function(compNameOrCommand, cmdName, args) {
var command;
if (_.isString(compNameOrCommand)) {
// arguments are command content
command = {
comp: compNameOrCommand,
cmd: cmdName,
args: args
};
}
else {
// arguments are command itself
command = compNameOrCommand;
}
Private: {
getDefaultConnection: function() {
return this.Instance.Libs[this.ComponentCommunications.getImplComponentLibName()];
}
}
}}),
Host: ComponentDef.defHost(function(SharedTools, Shared, SharedContext) {
var traceKeepOpenDepth;
// if buffering, just keep accumulating and send commands back later
this._buffer.commands.push(command);
var trace = function(name) {
//console.log(new Error(name).stack);
//console.log(name);
};
// return packet index
return this._buffer.commands.length-1;
},
var traceKeepOpen = function(which, nKeptOpen) {
// use a bit of magic to reduce the amount of stackframes displayed:
// Apply heuristics to ommit frames, originating from NoGap, internally
var frames = squishy.Stacktrace.getStackframesNotFromPath(__dirname);
var str = which + ' #' + nKeptOpen;
if (!frames) {
// internal call
str += ' (internal)';
}
else {
// call from user-code
str += ' @';
var n = Math.min(traceKeepOpenDepth, frames.length);
for (var i = 0; i < n; ++i) {
var frame = frames[i];
str += frame.fileName + ':' + frame.row + ':' + frame.column + '\n';
/**
* Add an array of commands to the buffer.
*/
bufferClientCommands: function(commands) {
this._buffer.commands.push.apply(this._buffer.commands, commands);
},
/**
* Compile request or response data.
* Includes all buffered commands, as well as given commandExecutionResults and errors.
* Resets the current command buffer.
*/
compilePacket: function(commandExecutionResults) {
var packetData = this._buffer;
packetData.commandExecutionResults = commandExecutionResults;
this._resetBuffer();
return packetData;
},
_resetBuffer: function() {
this._buffer = {
commands: [],
commandExecutionResults: null
};
}
console.log(str);
};
},
}),
var onNewConnection = function(conn) {
// do nothing for now
};
__ctor: function() {
console.assert(this.PacketBuffer);
},
/**
* A queue of command requests prevents crossing wires between two different sets of requests.
* Sets the charset for all transport operations.
* The default transport implementation bootstraps NoGap to the browser and
* uses this charset to populate the corresponding META tag.
*/
var CommandList = squishy.createClass(
function(instance, name) {
this.instance = instance;
this.name = name;
this.arr = [];
},{
put: function(cb) {
this.arr.push(cb);
},
/**
* Check if the given action can be executed now or enqueue/push it until it is safe to
* be run inside this instance's context.
*/
executeInOrder: function(cb, movesNext) {
if (this.instance.isBuffering()) {
// still executing a command: Queue the request.
this.put(cb);
}
else {
// go right ahead
this.instance.executeCbNow(cb);
if (!movesNext) {
this.instance.moveNext();
}
}
},
setCharset: function(charset) {
this.charset = charset;
},
/**
* Check if there are more callbacks in this list, and if so, call the next one.
*/
moveNext: function() {
var cb = this.remove();
//trace(new Error('moveNext').stack);
trace('moveNext');
if (cb) {
this.instance.executeCbNow(cb);
return true;
}
return false;
},
});
_getComponentTransportImplName: function(name) {
//return 'ComponentTransportImpl_' + name;
return '_ComponentTransportImpl';
},
var CommandQueue = squishy.extendClass(CommandList,
function(instance, name) {
this._super(instance, name);
},{
/**
* The current transport layer implementation
*/
getComponentTransportImpl: function() {
var transportImpl = Shared.Libs[this._getComponentTransportImplName()];
console.assert(transportImpl, 'Could not lookup the Host endpoint of the transport layer implementation.');
return transportImpl;
},
/**
* Creates a response packet, containing a single command
*/
createSingleCommandPacket: function(compName, cmdName, args) {
return {
commands: [{
comp: compName,
cmd: cmdName,
args: args
}]
};
},
Private: {
/**
* Remove and return oldest cb.
* This user's connection implementation state object
*/
remove: function() {
if (this.arr.length == 0) return null;
var cb = this.arr[0];
this.arr.splice(0, 1);
return cb;
getDefaultConnection: function() {
return this.Instance.Libs[this.ComponentCommunications._getComponentTransportImplName()];
},
});
var CommandStack = squishy.extendClass(CommandList,
function(instance, name) {
this._super(instance, name);
},{
/**
* Remove and return youngest cb.
* Get an identifier for the current user.
* For network transport layers (such as HTTP or WebSocket), this is usually the IP address.
* For WebWorkers and other kinds of environments that can be a custom name, assigned during
* initialization.
*/
remove: function() {
if (this.arr.length == 0) return null;
getUserIdentifier: function() {
return this.getDefaultConnection().getUserIdentifier();
},
var cb = this.arr[this.arr.length-1];
this.arr.splice(this.arr.length-1, 1);
return cb;
},
});
refresh: function() {
this.getDefaultConnection().client.refresh();
}
}
}}),
Host: ComponentDef.defHost(function(SharedTools, Shared, SharedContext) {
var onNewConnection = function(conn) {
// do nothing for now
};
var Promise;
return {

@@ -197,2 +192,3 @@ implementations: {},

__ctor: function() {
Promise = Shared.Libs.ComponentDef.Promise;
this.events = {

@@ -208,5 +204,5 @@ connectionError: squishy.createEvent(this)

squishy.assert(typeof(implType) === 'object',
'ComponentEndpointImpl definition must implement the `ComponentEndpointImpl` interface.');
'ComponentTransportImpl definition must implement the `ComponentTransportImpl` interface.');
squishy.assert(implType.ImplName, 'Unnamed ComponentEndpointImpl is illegal. Make sure to set the `ImplName` property.');
squishy.assert(implType.ImplName, 'Unnamed ComponentTransportImpl is illegal. Make sure to set the `ImplName` property.');

@@ -224,10 +220,8 @@ // store it in implementations

squishy.assert(this.implementations.hasOwnProperty(implName),
'ComponentEndpointImpl of type "' + implName + '" does not exist. Available types are: ' + Object.keys(this.implementations));
'ComponentTransportImpl of type "' + implName +
'" does not exist. Available types are: ' + Object.keys(this.implementations));
var implType = this.implementations[implName];
// remember trace config option
traceKeepOpenDepth = cfg.traceKeepOpen;
// register implementation as lib (so we also get access to it on the client side)
implType.Name = this.getImplComponentLibName();
implType.Name = this._getComponentTransportImplName();
ComponentDef.lib(implType);

@@ -243,16 +237,14 @@ },

Private: {
/**
* The queue handles client requests.
* Currently pending/executing Promise chain
*/
queue: null,
interceptStack: null,
sendListeners: [],
connectionData: {},
pendingResponsePromise: null,
hostResponse: null,
__ctor: function() {
this.queue = new CommandQueue(this);
this.interceptStack = new CommandStack(this);
this.keptOpen = 0;
this.moveNextCb = this.moveNext.bind(this);
this.hostResponse = new this.Shared.PacketBuffer();
this.pendingResponsePromise = Promise.resolve();
},

@@ -265,18 +257,9 @@

// ##############################################################################################################
// Methods to execute code while respecting the instance's action queue
// Handle requests
addSendListener: function(cb) {
this.sendListeners.push(cb);
},
/**
* Executes the given user requested callback in the right order.
* If the `movesNext` parameter should is set to true, the given callback must make sure
* to explicitely notify the queue to move forward, once finished.
* If set to false, `cb` will be handed another callback as first argument. That callback
* should be called once all actions triggered by `cb` have completed.
* @param {Bool} movesNext Whether the callback will explicitely move the queue forward.
* Whether this instance is currently running/has pending promises
*/
executeInOrder: function(cb, movesNext) {
this.queue.executeInOrder(cb, movesNext);
isExecutingRequest: function() {
return this.pendingResponsePromise.isPending();
},

@@ -286,249 +269,98 @@

* Used by endpoint implementations to execute and/or put commands.
* @return A promise that will return the Host's response to the given clientRequest.
*/
executeCommandRequest: function(commands, connectionState) {
var cb = function() {
this.executeCommandRequestNow(commands, connectionState);
}.bind(this);
this.queue.executeInOrder(cb, true);
handleClientRequest: function(clientRequest) {
// client can currently only send commands (no results etc.)
return this.executeCommandRequest(clientRequest.commands);
},
/**
* Calls code on host side that might raise commands to be sent to the clients.
* The commands will be sent through the given `connection` object.
* In order for this to happen, we temporarily store all current connection-related information on a stack and
* reset them on flush.
* Used by endpoint implementations to execute and/or put commands.
* @return A promise that will return a (potentially empty) return value for each of the requested commands.
*/
executeCommandRaisingCode: function(connection, code, doneCb, connectionState) {
var cb = function() {
// keep open
this.keepOpenInternal();
// override state, and add the code for resetting state to the queue
this.setConnectionOverride(connection, doneCb);
// setup command buffer
this.startRequest(connectionState);
// run code
code();
// flush
this.flush();
executeCommandRequest: function(commands) {
// start (or enqueue) command request
var next = function() {
return this.Instance.Libs.CommandProxy.executeClientCommands(commands);
}.bind(this);
// put request on intercept stack, so it will be the first thing to be executed after the current thing
this.interceptStack.executeInOrder(cb, true);
return this.executeInOrder(next);
},
// ##############################################################################################################
// Methods to be used for connection management (delivered by ComponentTools)
/**
* Prevent the current connection from closing.
* Make sure to call `flush`, once you are done.
* Execute the given function or promise once the all pending requests has been served.
* @return hostResponse data to be sent to client.
*/
keepOpen: function() {
if (!this.isBuffering()) {
console.error(new Error('Tried to keep open a connection that was alread flushed.').stack);
return;
executeInOrder: function(code) {
if (!this.pendingResponsePromise.isPending()) {
// queue is empty -> Start right away
// create new chain, so we don't keep all previous results until the end of time
this.pendingResponsePromise = code();
}
this.keepOpenInternal();
},
keepOpenInternal: function() {
++this.keptOpen;
if (traceKeepOpenDepth) {
traceKeepOpen('keepOpen', this.keptOpen-1);
}
},
/**
* Signals that an asynchronous operation completed.
* Flushes the current buffer, if this was the last pending asynchronous operation.
*/
flush: function() {
--this.keptOpen;
if (traceKeepOpenDepth) {
traceKeepOpen('flush', this.keptOpen);
}
if (this.keptOpen) return;
if (!this.isBuffering()) {
console.error(new Error('Tried to flush a connection that was alread flushed.').stack);
}
else {
// finally, finish up
this.finishRequest();
// there is still other stuff pending -> Wait until it's finished
this.pendingResponsePromise = this.pendingResponsePromise
.then(code);
}
},
return this.pendingResponsePromise
// ##############################################################################################################
// Internal: Queue management, command routing & connection state management
executeCommandRequestNow: function(commands, connectionState) {
this.keepOpenInternal();
// process commands and remember all commands to be sent back to client
this.startRequest(connectionState);
// start executing commands
this.Instance.Libs.CommandProxy.executeClientCommandsNow(commands);
this.flush();
// return client response, including commandExecutionResults
.then(this.hostResponse.compilePacket.bind(this.hostResponse));
},
/**
* Sends or buffers the given command request.
* Execute a piece of user code, wrapped in safety measures.
* Returns a promise, yielding the serialized result of the code execution.
*/
sendCommandToClient: function(compName, cmdName, args) {
if (this.isBuffering()) {
// if buffering, just keep accumulating and send commands back later
var buf = this.connectionData.commandBuffer;
buf.push({
comp: compName,
cmd: cmdName,
args: args
});
}
else if (!this.connectionData.connectionState) {
var cmdName = compName + '.Public.' + cmdName;
console.error(new Error('Tried to execute command `' + cmdName + '` on client while no client was connected. ' +
'Make sure to use `this.Tools.keepOpen` and `this.Tools.flush` when performing asynchronous operations.').stack);
}
else {
// send command right away
var commands = this.ComponentCommunications.createSingleCommandPacket(compName, cmdName, args);
executeUserCodeAndSerializeResult: function(code) {
return Promise.resolve()
.bind(this)
.then(code)
.then(function(returnValue) {
// wrap return value
return {
value: returnValue,
err: null
};
})
.catch(function(err) {
// wrap error
this.Tools.handleError(err);
var isException = !!(err && err.stack);
return {
value: null,
var connectionImpl = this.getCurrentConnection();
connectionImpl.sendCommandsToClient(commands, this.connectionData.connectionState);
}
// only send back message if error has stack
err: isException && 'error.internal' || err
};
});
},
isBuffering: function() {
return !!this.connectionData.commandBuffer;
},
/**
* We are about to execute code that will produce commands to be sent to the client.
* Return and reset current hostResponse buffer.
*/
startRequest: function(connectionState) {
// remember all commands to be sent back to client
this.connectionData.commandBuffer = [];
compileHostResponse: function(returnValues) {
return this.hostResponse.compilePacket(returnValues);
}
// remember the implementation-specific connection state object
this.connectionData.connectionState = connectionState;
trace('startRequest');
},
// /**
// * Skip the queue and run given code right away, while buffering
// * all resulting commands to be sent back to client.
// */
// executeCommandRaisingCodeNow: function(commandRaisingCode) {
// // override response buffer
// var originalBuffer = this.hostResponse;
// var newBuffer = this.hostResponse = new PacketBuffer();
/**
* We have finished executing code that produced commands to be sent to the client.
* Now send all collected commands back.
*/
finishRequest: function() {
// get current connection
var connection = this.getCurrentConnection();
// return Promise.resolve(commandRaisingCode)
// .bind(this)
// .then(function(commandExecutionResults) {
// // reset buffer
// this.hostResponse = originalBuffer;
// commands finished executing; now send all collected commands back to client in one chunk
connection.sendCommandsToClient(this.connectionData.commandBuffer, this.connectionData.connectionState);
// call send listeners
for (var i = 0; i < this.sendListeners.length; ++i) {
this.sendListeners[i]();
}
// remove listeners (they are only one-shot)
this.sendListeners.length = 0;
trace('finishRequest');
// unset stuff
this.connectionData.commandBuffer = null;
this.connectionData.connectionState = null;
this.unsetConnectionOverride();
this.moveNext();
},
moveNext: function() {
// check intercepts
if (!this.interceptStack.moveNext()) {
// no intercepts -> move queue forward
this.queue.moveNext();
}
},
executeCbNow: function(cb) {
cb(this.moveNextCb);
},
getOverrideConnection: function() {
var internalContext = this.Instance.Libs.ComponentContext.getInternalContext();
if (internalContext.currentConnection) {
return internalContext.currentConnection;
}
return null;
},
/**
* Get an explicitely assigned connection object, or the
* instance of the currently used connection implementation.
*/
getCurrentConnection: function() {
// first check if the connection implementation was overridden
var internalContext = this.Instance.Libs.ComponentContext.getInternalContext();
if (internalContext.currentConnection) {
return internalContext.currentConnection;
}
// if not, return default
return this.getDefaultConnection();
},
/**
* Tell this library to route all communication for the current instance through the given
* connection object for now.
* Remember the original connection state and reset it, once all code for this connection has been executed.
*/
setConnectionOverride: function(connection, doneCb) {
// store all internal state and override connection
var internalContext = this.Instance.Libs.ComponentContext.getInternalContext();
internalContext.currentConnection = connection;
internalContext.setConnectionOverrideCb = doneCb;
trace('setConnectionOverride');
},
/**
* Clean up a connection override (if there was any) and call pending callback.
*/
unsetConnectionOverride: function() {
trace('unsetConnectionOverride');
var internalContext = this.Instance.Libs.ComponentContext.getInternalContext();
var cb = internalContext.setConnectionOverrideCb;
if (cb) {
// clean up
internalContext.currentConnection = null;
internalContext.setConnectionOverrideCb = null;
// call the cb
cb();
}
},
// ##############################################################################################################
// Reply to a pending `onReply` callback on the client side.
sendReply: function(replyId, args) {
this.client.returnReply(replyId, args);
}
// // give back response data to be sent to client
// return newBuffer.compilePacket(commandExecutionResults);
// });
// },
}

@@ -540,36 +372,19 @@ };

Client: ComponentDef.defClient(function(Tools, Instance, Context) {
var timer;
var buffer = [];
var lastReplyId = 0;
var pendingCbs = {};
var Promise; // Promise library
var sendBufferToHost = function() {
// send out buffer;
thisInstance.getDefaultConnection().sendCommandsToHost(buffer);
return {
_requestPromise: null,
_requestPacket: null,
// clear buffer
buffer.length = 0;
__ctor: function() {
Promise = Instance.Libs.ComponentDef.Promise;
this._requestPacket = new this.PacketBuffer();
},
// unset timer
timer = null;
};
var Packet = {
onReply: function(cb) {
this.replyId = ++lastReplyId;
pendingCbs[this.replyId] = cb;
return this; // return self for further chaining
}
};
var thisInstance;
return thisInstance = {
/**
* Add command to host, and send out very soon as part of a (small) batch.
*/
sendCommandToHost: function(compName, cmdName, args) {
// build packet & store in buffer
var cmdPacket = Object.create(Packet);
cmdPacket.comp = compName;
cmdPacket.cmd = cmdName;
cmdPacket.args = args;
buffer.push(cmdPacket);
// add command to buffer
var returnIndex = this._requestPacket.bufferCommand(compName, cmdName, args);

@@ -579,24 +394,57 @@ // do not send out every command on its own;

// a batch of all commands together
if (!timer) {
timer = setTimeout(sendBufferToHost, 1);
if (!this._requestPromise) {
this._requestPromise = Promise.delay(1)
.bind(this)
.then(this._sendClientRequestBufferToHost);
}
return cmdPacket;
},
Public: {
/**
* Host has replied to our pending `onReply` request.
*/
returnReply: function(replyId, args) {
// get cb
var cb = pendingCbs[replyId];
if (!cb) {
console.warn('Host sent return reply with invalid `replyId`: ' + replyId);
return;
// send the corresponding return value back to caller
return this._requestPromise
.then(function(commandExecutionResults) {
var result = commandExecutionResults && commandExecutionResults[returnIndex];
if (!result) {
// nothing to return
return null;
}
else if (!result.err) {
// return result value
return result.value;
}
else {
// reject
return Promise.reject(result.err);
}
});
},
// delete cb from set and execute it
delete pendingCbs[replyId];
cb.apply(null, args);
/**
* Actually send Client request to Host.
* Compile response packet for client; includes all buffered commands.
* Resets the current commandBuffer.
*/
_sendClientRequestBufferToHost: function() {
this._requestPromise = null; // reset promise
// compile and send out data
var clientRequest = this._requestPacket.compilePacket();
return this.getDefaultConnection().sendClientRequestToHost(clientRequest)
// once received, handle reply sent back by Host
.then(this.handleHostResponse);
},
/**
* Host sent stuff. Run commands and return the set of returnValues.
*/
handleHostResponse: function(hostReply) {
if (hostReply.commands) {
// execute commands sent back by Host
Instance.Libs.CommandProxy.executeHostCommands(hostReply.commands);
}
// send return values sent by host back to callers
return hostReply && hostReply.commandExecutionResults;
},
Public: {
}

@@ -646,8 +494,75 @@ };

*/
deserialize: function(objString, includeCode) {
if (includeCode) {
return eval(objString);
deserialize: function(objString, evaluateWithCode) {
if (evaluateWithCode) {
return eval('(' + objString + ')');
}
return JSON.parse(objString);
}
},
/**
* Asset handlers are given to the Assets library for initializing assets.
*/
assetHandlers: {
/**
* Functions to fix asset filenames of given types.
*/
autoIncludeResolvers: {
js: function(fname) {
if (!fname.endsWith('.js')) fname += '.js';
return fname;
},
css: function(fname) {
if (!fname.endsWith('.css')) fname += '.css';
return fname;
}
},
/**
* Functions to generate code for including external file assets.
* Also need to fix tag brackets because this string will be part of a script
* that actually writes the asset code to the HTML document.
*
* @see http://stackoverflow.com/a/236106/2228771
*/
autoIncludeCodeFactories: {
js: function(fname) {
return '\x3Cscript type="text/javascript" src="' + fname + '">\x3C/script>';
},
css: function(fname) {
return '\x3Clink href="' + fname + '" rel="stylesheet" type="text/css">\x3C/link>';
},
/**
* Unsupported format: Provide the complete include string.
*/
raw: function(fname) {
return fname;
}
}
},
Private: {
getUserIdentifier: function() {
// Not Yet Implemented on Client
if (SharedContext.IsHost) {
var req = this._lastReq;
var ipStr = null;
if (req) {
// try to get IP
ipStr = req.headers['x-forwarded-for'] ||
(req.socket && req.socket.remoteAddress) ||
(req.connection &&
(req.connection.remoteAddress ||
(req.connection.socket && req.connection.socket.remoteAddress)));
}
return ipStr;
}
else {
// simple
return null;
}
}
}

@@ -664,34 +579,77 @@ };

initHost: function(app, cfg) {
// set charset
this.charset = cfg.charset;
// pre-build <script> & <link> includes
this.includeCode = Shared.Libs.ComponentAssets.getAutoIncludeAssets(this.assetHandlers);
},
/**
* Initialize host-side endpoint implementation (which delivers the low-level mechanism to transfer command requests between client & host).
* Initialize host-side endpoint implementation.
* This delivers the low-level mechanism to transfer command requests between client & host.
*/
initHost: function(app, cfg) {
bootstrap: function(app, cfg) {
this._startBootstrapRequestListener(app, cfg);
this._startRPCRequestListener(app, cfg);
},
_activateInstanceForSession: function(req, force) {
var session = req.session;
var sessionId = req.sessionID;
console.assert(session,
'req.session was not set. Make sure to use a session manager before the components library, when using the default Get bootstrapping method.');
console.assert(sessionId,
'req.sessionID was not set. Make sure to use a compatible session manager before the components library, when using the default Get bootstrapping method.');
// get or create Instance map
var Instance = Shared.Libs.ComponentInstance.activateSession(session, sessionId, force);
// console.assert(Shared.Libs.ComponentInstance.activateSession(session, sessionId),
// 'Session activation failed.');
return Instance;
},
_startBootstrapRequestListener: function(app, cfg) {
// listen for Client bootstrap requests
app.get(cfg.baseUrl + "*", function(req, res, next) {
// register error handler to avoid application crash
var onError = function(err) {
Shared.Libs.ComponentCommunications.reportConnectionError(req, res, err);
next(err);
};
req.on('error', onError);
// This will currently cause bugs
// see: https://github.com/mikeal/request/issues/870
// req.socket.on('error', onError);
console.log('Incoming client requesting `' + req.url + '`');
var Instance = this._activateInstanceForSession(req, true);
// handle the request
var implementationInstance = Instance.Libs.ComponentCommunications.getDefaultConnection();
implementationInstance._handleClientBootstrapRequest(req, res, next);
}.bind(this));
},
_startRPCRequestListener: function(app, cfg) {
squishy.assert(app.post, 'Invalid argument for initHost: `app` does not have a `post` method. ' +
'Make sure to pass an express application object to `ComponentLoader.start` when using ' +
'NoGap\'s default HttpPost implementation.');
// TODO: Add CSRF security!!!
// define router callback
var cb = function(req, res, next) {
// listen for Client RPC requests
app.post(cfg.baseUrl, function(req, res, next) {
// register error handler
var onError = function(err) {
Shared.Libs.ComponentCommunications.reportConnectionError(req, res, err);
next(err);
};
req.on('error', onError);
// This will currently cause bugs
// see: https://github.com/mikeal/request/issues/870
// req.socket.on('error', onError);
// extract body data
// see: http://stackoverflow.com/a/4310087/2228771
var session = req.session;
var sessionId = req.sessionID;
console.assert(session,
'req.session was not set. Make sure to use a session manager before the components library, when using the default Get bootstrapping method.');
console.assert(sessionId,
'req.sessionID was not set. Make sure to use a compatible session manager before the components library, when using the default Get bootstrapping method.');
// TODO: Add CSRF security
var body = '';

@@ -702,41 +660,46 @@ req.on('data', function (data) {

req.on('end', function () {
var commands;
var clientRequest;
try {
commands = serializer.deserialize(body);
clientRequest = serializer.deserialize(body);
}
catch (err) {
console.warn('Invalid data sent by client: ' + body + ' -- Error: ' + err.message || err);
next();
// empty request is invalid
err = new Error('Invalid data sent by client cannot be parsed: ' +
body + ' -- Error: ' + err.message || err);
next(err);
return;
}
if (!commands) return;
if (!clientRequest) {
// empty request is invalid
next(new Error('Empty client request'));
return;
}
// set session & get instance object
// TODO: Check if that works with parallel requests
var Instance = Shared.Libs.ComponentInstance.activateSession(session, sessionId);
var Instance = this._activateInstanceForSession(req, false);
if (!Instance) {
// use a bit of a hack to tell the client to refresh current installation:
commands = Shared.Libs.ComponentCommunications.createSingleCommandPacket(
Shared.Libs.ComponentBootstrap.getImplComponentLibName(), 'refresh');
this._def.InstanceProto.sendCommandsToClient(commands, res);
// Client sent a command but had no cached instance.
// Tell Client to refresh (assuming the client's currently running Bootstrapper implementation supports it):
var responsePacket = Shared.Libs.ComponentCommunications.createSingleCommandPacket(
Shared.Libs.ComponentCommunications._getComponentTransportImplName(), 'refresh');
this._def.InstanceProto.sendResponseToClient(responsePacket, res);
return;
}
Instance.Libs.ComponentContext.touch(); // update last used time
// execute actions
Instance.Libs.ComponentCommunications.executeCommandRequest(commands, res);
// handle the request
var implementationInstance = Instance.Libs.ComponentCommunications.getDefaultConnection();
implementationInstance._handleClientRPCRequest(req, res, next, clientRequest);
}.bind(this));
}.bind(this);
// register router callback
app.post(cfg.baseUrl, cb)
.on('error', function(err) {
console.error('Connection error during HTTP get: ' + err);
});;
}.bind(this));
},
setGlobal: function(varName, varValue) {
GLOBAL[varName] = varValue;
},
Private: {
__ctor: function() {
},
onClientBootstrap: function() {

@@ -748,29 +711,106 @@ var clientRoot = this.Context.clientRoot;

// If we don't have that, the client side of the component framework does not know how to send commands.
this.client.setUrl(clientRoot);
},
this.client.setConfig({
remoteUrl: clientRoot,
charset: this.Shared.charset
});
},
staysOpen: function() {
return false;
_handleClientBootstrapRequest: function(req, res, next) {
this._lastReq = req;
// get client root, so we know what address the client sees
var clientRoot = req.protocol + '://' + req.get('host');
var remoteAddr = req.connection.remoteAddress;
var ComponentBootstrapInstance = this.Instance.Libs.ComponentBootstrap;
// install new instance and generate client-side code
return ComponentBootstrapInstance.bootstrapComponentInstance(remoteAddr, clientRoot)
.bind(this)
.catch(function(err) {
// something went wrong
//debugger;
ComponentBootstrapInstance.Tools.handleError(err);
// error!
next(err);
})
// send bootstrap code to Client and kick things of there
.then(function(codeString) {
if (!codeString) return; // something went wrong
// determine charset
var charset = (this.charset || 'UTF-8');
// fix </script> tags in bootstrapping code
codeString = codeString.replace(/<\/script>/g, '\\x3c/script>');
// send out bootstrapping page to everyone who comes in:
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('<!doctype html>\n<html><head>');
// see: http://www.w3schools.com/tags/att_meta_charset.asp
res.write('<meta charset="' + charset + '" />');
// write CSS + JS external files
res.write(this.Shared.includeCode);
// done with head
res.write('</head><body>');
// write NoGap bootstrapping code
res.write('<script type="text/javascript" charset="' + charset + '">');
res.write('eval(eval(' + JSON.stringify(codeString) + '))');
res.write('</script>');
// wrap things up
res.write('</body></html>');
res.end();
})
.finally(function() {
//console.log('Finished serving client: `' + req.url + '`');
})
// sending out the response went wrong
.catch(ComponentBootstrapInstance.Tools.handleError.bind(ComponentBootstrapInstance.Tools));
},
_handleClientRPCRequest: function(req, res, next, clientRequest) {
// remember request object
this._lastReq = req;
var Tools = this.Instance.Libs.ComponentCommunications.Tools;
// Execute commands.
// Once finished executing, `sendResponseToClient` will be called
// with the response packet to be interpreted on the client side.
return this.Instance.Libs.ComponentCommunications.handleClientRequest(clientRequest)
.then(function(hostResponse) {
this.sendResponseToClient(hostResponse, res);
}.bind(this))
// catch and handle any error
.catch(Tools.handleError.bind(Tools));
},
/**
* This host-side function is called when a bunch of client commands are to be sent to client.
* This Host-side function is called when a bunch of Client commands are to be sent to Client.
*/
sendCommandsToClient: function(commands, res) {
console.assert(res, 'INTERNAL ERROR: `connectionState` was not set.');
sendResponseToClient: function(response, res) {
console.assert(res, 'Tried to call `sendResponseToClient` without `res` connection state object.');
var commStr;
// if response object is given, send right away
var commandStr;
try {
// Serialize
commStr = serializer.serialize(commands);
commandStr = serializer.serialize(response);
}
catch (err) {
// delete args, so we can atually produce a string representation of the commands
for (var i = 0; i < commands.length; ++i) {
delete commands[i].args;
}
// produce pruned string representation
var commStr = squishy.objToString(response, true, 3);
// re-compute string representation, without complex arguments
// This *MUST* work, unless there is a bug in the packaging code in this file.
commStr = squishy.objToString(commands, true);
res.statusCode = 500;
res.end();

@@ -780,15 +820,11 @@ // then report error

'[NoGap] Invalid remote method call: Tried to send too complicated object. ' +
'Arguments to remote methods must be simple objects or functions (or a mixture thereof). ' +
'If the failed commands contain `ComponentCommunications.returnReply`, ' +
'this error was caused by the arguments of a call to `client.reply`.\n' +
'Arguments to remote methods must be simple objects or functions (or a mixture thereof).\n' +
'Failed commands:\n ' + commStr + '. ' );
}
// flush response & close connection
res.contentType('application/json');
res.setHeader("Access-Control-Allow-Origin", "*");
res.write(commStr);
res.write(commandStr);
res.end();
res = null;
},

@@ -798,3 +834,2 @@ }

}),

@@ -805,22 +840,22 @@ /**

Client: ComponentDef.defClient(function(Tools, Instance, Context) {
var clientUrl;
var cfg;
var serializer;
var Promise;
return {
__ctor: function() {
Promise = Instance.Libs.ComponentDef.Promise;
serializer = this.Serializer;
},
Public: {
setUrl: function(newClientUrl) {
clientUrl = newClientUrl;
}
setGlobal: function(varName, varValue) {
window[varName] = varValue;
},
Private: {
/**
* This client-side function is called when a host command is called from a client component.
* It will transport the commands to the host, wait for the reply, and then execute the commands that were sent back.
*/
sendCommandsToHost: function(commands) {
/**
* This client-side function is called when a host command is called from a client component.
* It will transport the commands to the host, wait for the reply, and then execute the commands that were sent back.
*/
sendClientRequestToHost: function(clientRequest) {
var promise = new Promise(function(resolve, reject) {
// send Ajax POST request (without jQuery)

@@ -830,8 +865,11 @@ var xhReq = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");

// send out the command request
//console.log(clientUrl);
xhReq.open('POST', clientUrl, true);
//xhReq.setRequestHeader('Content-type','application/json; charset=utf-8;');
xhReq.setRequestHeader('Content-type','application/json');
//console.log(cfg.remoteUrl);
xhReq.open('POST', cfg.remoteUrl, true);
xhReq.setRequestHeader('Content-type','application/json; charset=' + (cfg.charset || 'utf-8') + ';');
//xhReq.setRequestHeader('Content-type','application/json');
xhReq.onerror = function() {
console.error('AJAX request failed: ' + xhReq.responseText);
// TODO: Better error handling
// network-level failure
var err = 'AJAX request failed: ' + xhReq.responseText;
reject(err);
};

@@ -842,29 +880,48 @@ xhReq.onreadystatechange = function() {

if (xhReq.status==200) {
if (xhReq.responseText) {
// host sent commands back, in response to our execution request
var hostCommands;
try {
// Deserialize
hostCommands = serializer.deserialize(xhReq.responseText, true);
}
catch (err) {
console.error('Unable to parse commands sent by host: ' + err + ' -- \n' + xhReq.responseText);
return;
}
// host sent hostReply back, in response to our execution request
var hostReply;
try {
// Deserialize
hostReply = serializer.deserialize(xhReq.responseText || '', true) || {};
}
catch (err) {
console.error(err.stack);
// TODO: Better error handling
err = 'Unable to parse reply sent by host. '
+ 'Check out http://jsonlint.com/ for more information. - \n'
+ xhReq.responseText;
reject(err);
return;
}
// execute the hostCommands
Instance.Libs.CommandProxy.execHostCommands(hostCommands);
}
// return host-sent data to caller
resolve(hostReply);
}
else {
console.error('Invalid status from host: ' + xhReq.responseText);
// TODO: Better error handling
// application-level failure
var err = new Error('Invalid status from host: ' + xhReq.status +
' \n' + xhReq.responseText);
reject(err);
}
};
// send commands
var commandStr = serializer.serialize(commands);
xhReq.send(commandStr);
// send request
var requestData = serializer.serialize(clientRequest);
xhReq.send(requestData);
});
return promise;
},
Public: {
setConfig: function(newCfg) {
cfg = newCfg;
},
refresh: function() {
console.log('Page refresh requested. Refreshing...');
window.location.reload();
}
}
},
};

@@ -871,0 +928,0 @@ })

@@ -32,4 +32,4 @@ /**

* This is the code that runs on host & client to bootstrap each side's data.
* This is the special-purpose version of `installComponent` for installing the `ComponentDef` library component itself.
* NOTE: If you change this, also make sure to check `installComponent`.
* This is the special-purpose version of `_installComponent` for installing the `ComponentDef` library component itself.
* NOTE: If you change this, also make sure to check `_installComponent`.
*/

@@ -42,6 +42,2 @@ bootstrapSelf: CodeBuilder.serializeInlineFunction(function (ComponentDefDef, endpointName) {

var ComponentDefBase = ComponentDefDef.Base.factoryFun();
// call and remove Base ctor
ComponentDefBase.__ctor();
delete ComponentDefBase.__ctor;

@@ -52,16 +48,30 @@ // get endpoint definition & set name

// install Client and/or Host
var ComponentDefEndpoint = ComponentDefBase.createComponentFromDef(def);
// call Base ctor
// IMPORTANT: Must call before calling `_createComponentFromDef`
ComponentDefBase.__ctor();
// create Client / Host endpoint
var ComponentDefEndpoint = ComponentDefBase._createComponentFromDef(def);
var endpointCtor;
// get and remove endpoint ctor
if (ComponentDefEndpoint.__ctor) {
endpointCtor = ComponentDefEndpoint.__ctor;
delete ComponentDefEndpoint.__ctor;
}
// merge ComponentDef's base with Client/Host
ComponentDefEndpoint = ComponentDefBase.mergeBaseIntoComponentEndpoint(def, endpointName, ComponentDefBase, ComponentDefEndpoint);
ComponentDefEndpoint = ComponentDefBase._mergeBaseIntoComponentEndpoint(def, endpointName, ComponentDefBase, ComponentDefEndpoint);
// call and remove endpoint ctor
if (ComponentDefEndpoint.__ctor) {
ComponentDefEndpoint.__ctor();
delete ComponentDefEndpoint.__ctor;
// call ctor
if (endpointCtor) {
endpointCtor.call(ComponentDefEndpoint);
}
// special treatment for ComponentDef initialization
ComponentDefEndpoint._initBase_internal();
// final touches on `Host` installation of ComponentDef
ComponentDefBase.finishInstalledComponentEndpoint(ComponentDefEndpoint, ComponentDefBase, ComponentDefEndpoint.SharedComponents.Libs);
ComponentDefBase._finishInstalledComponentEndpoint(ComponentDefEndpoint, ComponentDefBase, ComponentDefEndpoint.SharedComponents.Libs);

@@ -139,3 +149,5 @@ return ComponentDefEndpoint;

*/
Base: HostCore.defBase(function(Tools, Shared, Context) {
Base: HostCore.defBase(function(SharedTools, Shared, SharedContext) {
var Promise;
/**

@@ -160,3 +172,4 @@ * @constructor

value: {
catName: catName
catName: catName,
componentArray: []
}

@@ -173,2 +186,8 @@ });

});
Object.defineProperty(this, 'getComponents', {
value: function() {
return this._data.componentArray;
}.bind(this)
});

@@ -178,2 +197,3 @@ Object.defineProperty(this, 'addComponent', {

this[compName] = component;
this._data.componentArray.push(component);
}

@@ -185,4 +205,5 @@ })

Object.defineProperty(componentEndpoint, publicPrivate, {
enumerable: false,
get: function() {
throw new Error('Tried to access `Shared.' + componentEndpoint._def.getFullInstanceMemberName(publicPrivate) +
throw new Error('Tried to access `Shared.' + publicPrivate +
'`. Access it\'s methods on an instance of that component instead (by e.g. calling `this.Instance.' +

@@ -192,2 +213,4 @@ componentEndpoint._def.FullName + '.someMethod(...)` or simply `this.someMethod(...)`.');

});
console.assert(!componentEndpoint.propertyIsEnumerable(publicPrivate), '`defineProperty` failed hard.');
};

@@ -198,2 +221,3 @@

return {
DEBUG: 1,
SharedComponents: undefined,

@@ -204,6 +228,17 @@ SharedTools: {},

__ctor: function() {
Shared = this.SharedComponents = this.createComponentMap(true);
Shared = this.SharedComponents = this._createComponentMap(true);
SharedTools = this.SharedTools;
SharedContext = this.SharedContext;
},
_initBase_internal: function() {
// Promise has been added to ComponentDef in corresponding ctor
Promise = this.Promise;
console.assert(!!Promise, 'Could not load Promise library.');
// TODO: Look at configuration to determine whether to use longStackTraces or not
Promise.longStackTraces();
},
// ################################################################################################################

@@ -215,3 +250,3 @@ // Component maps (Shared & Instance objects)

*/
createComponentMap: function(isShared) {
_createComponentMap: function(isShared) {
var map = new ComponentMap(isShared ? 'Shared Component' : 'Component Instance');

@@ -253,5 +288,5 @@

*/
createComponentFromDef: function(codeDef) {
_createComponentFromDef: function(codeDef) {
// add Components and a pseudo-global called `Context`
return codeDef.factoryFun(this.SharedTools, Shared, this.SharedContext);
return codeDef.factoryFun(SharedTools, Shared, SharedContext);
},

@@ -264,3 +299,3 @@

*/
finishInstalledComponentEndpoint: function(componentEndpoint, base, sharedMap) {
_finishInstalledComponentEndpoint: function(componentEndpoint, base, sharedMap) {
var def = componentEndpoint._def;

@@ -286,3 +321,3 @@

// create merged instance proto and add to definition:
def.InstanceProto = this.doMerge('Component endpoint `' + def.FullName + '.' + def.type + '`',
def.InstanceProto = this._doMergeEndpoints('Component endpoint `' + def.FullName + '.' + def.type + '`',
'Private', 'Public', componentEndpoint.Private, componentEndpoint.Public) || {};

@@ -305,3 +340,3 @@

*/
installComponent: function(sharedMap, componentDef, endpointName) {
_installComponent: function(sharedMap, componentDef, endpointName) {
var baseDef = componentDef.Base;

@@ -317,4 +352,4 @@ var endpointDef = componentDef[endpointName];

// call factory function
var base = baseDef ? this.createComponentFromDef(baseDef) : {};
var componentEndpoint = endpointDef ? this.createComponentFromDef(endpointDef) : {};
var base = baseDef ? this._createComponentFromDef(baseDef) : {};
var componentEndpoint = endpointDef ? this._createComponentFromDef(endpointDef) : {};

@@ -335,3 +370,3 @@ // get & delete ctors before merging

// merge and then store definition in _def
componentEndpoint = this.mergeBaseIntoComponentEndpoint(
componentEndpoint = this._mergeBaseIntoComponentEndpoint(
endpointDef, endpointName, base, componentEndpoint);

@@ -348,3 +383,3 @@

// do some useful & necessary modifications on componentEndpoint
this.finishInstalledComponentEndpoint(componentEndpoint, base, sharedMap);
this._finishInstalledComponentEndpoint(componentEndpoint, base, sharedMap);

@@ -364,3 +399,3 @@ //console.log('Installed componentEndpoint: ' + name);

*/
mergeBaseIntoComponentEndpoint: function(componentDef, nameTo, base, componentEndpoint) {
_mergeBaseIntoComponentEndpoint: function(componentDef, nameTo, base, componentEndpoint) {
// store base instance ctor separately, so it won't be overwritten

@@ -374,3 +409,3 @@ var __baseCtor = base.Private && base.Private.__ctor;

// merge Base into endpoint
var componentEndpoint = this.doMerge('Component `' + componentDef.FullName + '`', 'Base',
var componentEndpoint = this._doMergeEndpoints('Component `' + componentDef.FullName + '`', 'Base',
nameTo, base, componentEndpoint, true);

@@ -388,3 +423,3 @@

*/
doMerge: function(ownerName, nameFrom, nameTo, from, to, allowOverrides, level) {
_doMergeEndpoints: function(ownerName, nameFrom, nameTo, from, to, allowOverrides, level) {
level = level || 0;

@@ -406,3 +441,3 @@ if (level > 12) {

// nested object -> Merge recursively
to[propName] = this.doMerge(ownerName,
to[propName] = this._doMergeEndpoints(ownerName,
nameFrom + '.' + propName,

@@ -443,3 +478,5 @@ nameTo + '.' + propName,

*/
onAfterAllComponentsInstalled: function(component) {
_onAfterAllComponentsInstalled: function(component) {
// This is dangerous since it can mess up constructors and other special kinds of functions
//SharedTools.bindAllMethodsToObject(component); // components are singletons, so no harm done!
Shared.Libs.ComponentInstance.onComponentInstallation(component);

@@ -453,3 +490,3 @@ }

*/
Host: HostCore.defHost(function(Tools, Shared) {
Host: HostCore.defHost(function(SharedTools, Shared, SharedContext) {
// ##############################################################################################################

@@ -540,3 +577,3 @@ // private static members

*/
var installComponentOnHost = function(componentDef, clientDefs, map, creationFrame) {
var _installComponentOnHost = function(componentDef, clientDefs, map, creationFrame) {
// get all command names and make them ready

@@ -563,3 +600,3 @@ fixComponentDefinition(componentDef, 'Host');

// install and return host component
return Shared.Libs.ComponentDef.installComponent(map, componentDef, 'Host');
return Shared.Libs.ComponentDef._installComponent(map, componentDef, 'Host');
};

@@ -570,3 +607,3 @@

*/
var installClientCode = CodeBuilder.serializeInlineFunction(function(clientData) {
var _clientInstallCode = CodeBuilder.serializeInlineFunction(function(clientData) {
// due to some weird bug in chrome, we sometimes only get a meaningful stacktrace in the browser

@@ -596,3 +633,3 @@ // if we catch it through the global error handler, and often only after a second try.

// get `the definition of ComponentDef`
// get the definition of `ComponentDef` itself
var ComponentDefDef = compDefData.ComponentDefDef;

@@ -608,3 +645,3 @@

// post-installation code
ComponentDef.initClientComponents(libDefs, SharedComponents.Libs);
ComponentDef.initClientComponents(libDefs, SharedComponents.Libs, true);

@@ -618,3 +655,2 @@ // libs have been installed; now let ComponentBootstrap do the rest

return {

@@ -632,3 +668,24 @@ // public members

this.clientDefs = clientDefs;
// add Promise library on Host
if (typeof(Promise) !== 'undefined') {
this.Promise = Promise;
}
else {
this.Promise = require('../assets/bluebird.min');
}
// assign these guys
Shared = this.SharedComponents;
SharedTools = this.SharedTools;
SharedContext = this.SharedContext;
},
/**
* Get the names of all components to be sent to the Client on bootstrap.
* That is all, their `Client` and `Base` definitions.
*/
getAllClientDefinitions: function() {
return clientDefs;
},

@@ -697,3 +754,3 @@ // ##############################################################################################################

return installComponentOnHost(def, defs || clientDefs.Components, map || Shared, creationFrame);
return _installComponentOnHost(def, defs || clientDefs.Components, map || Shared, creationFrame);
},

@@ -708,25 +765,34 @@

*/
initializeHostComponents: function(app, cfg) {
_initializeHostComponentsAsync: function(app, cfg) {
// fix up all shared component objects
Shared.forEachComponentOfAnyType(function(component) {
this.onAfterAllComponentsInstalled(component)
this._onAfterAllComponentsInstalled(component)
}.bind(this));
// call initBase + initHost on all libs, with some special arguments
Shared.Libs.forEach(function(component) {
// call initBase + initHost on native components
return Promise.map(Shared.Libs.getComponents(), function(component) {
if (component.initBase) {
component.initBase();
return component.initBase();
}
})
.return(Shared.Libs.getComponents())
.map(function(component) {
if (component.initHost) {
component.initHost(app, cfg);
return component.initHost(app, cfg);
}
});
// call initBase + initHost on all other components
Shared.forEach(function(component) {
})
// call initBase + initHost on other components
.return(Shared.getComponents())
.map(function(component) {
if (component.initBase) {
component.initBase();
return component.initBase(app, cfg);
}
})
.return(Shared.getComponents())
.map(function(component) {
if (component.initHost) {
component.initHost(app, cfg);
return component.initHost(app, cfg);
}

@@ -738,7 +804,7 @@ });

* Returns the component installer function.
* That function contains the code to install the components framework on a new client.
* That function contains the code to install NoGap on a new client.
*
* @param {Object} bootstrapData The data to be given to ComponentBootstrap after ComponentDef took care of the basics.
*/
getClientInstallCode: function(bootstrapData) {
_buildClientInstallCode: function(bootstrapData) {
// define arguments of function call

@@ -748,7 +814,8 @@ var compDefData = {

code: {
//_initLibraries: HostCore._initLibraries,
bootstrapSelf: HostCore.bootstrapSelf
},
// add all library definitions
libDefs: clientDefs.Libs.list
// add all NoGap Client component definitions
libDefs: this.getAllClientDefinitions().Libs.list
};

@@ -770,3 +837,3 @@

// build installer function call with initializer data as argument
return CodeBuilder.buildFunctionCall(installClientCode, clientData);
return CodeBuilder.buildFunctionCall(_clientInstallCode, clientData);
},

@@ -781,3 +848,3 @@

*/
getClientComponentDefsForDeployment: function(componentNames) {
_getClientComponentDefsForDeployment: function(componentNames) {
var defs = [];

@@ -842,5 +909,5 @@ var uniqueNames = {};

__ctor: function() {
Tools = this.Tools;
Instance = this.SharedComponents;
__ctor: function() {
// add Promise library on Client
this.Promise = Promise;
},

@@ -854,3 +921,3 @@

var def = defs[i];
this.installComponent(map, def, 'Client');
this._installComponent(map, def, 'Client');
}

@@ -862,3 +929,3 @@ },

*/
initClientComponents: function(defs, map) {
initClientComponents: function(defs, map, isInternal) {
// do some general stuff first

@@ -868,3 +935,3 @@ for (var i = 0; i < defs.length; ++i) {

var comp = map[endpointDef.FullName];
this.onAfterAllComponentsInstalled(comp);
this._onAfterAllComponentsInstalled(comp);
}

@@ -877,5 +944,7 @@

if (comp.initBase) {
Tools.traceComponentFunctionCall(endpointDef.FullName, 'initBase');
comp.initBase();
}
if (comp.initClient) {
Tools.traceComponentFunctionCall(endpointDef.FullName, 'initClient');
comp.initClient();

@@ -882,0 +951,0 @@ }

@@ -12,2 +12,4 @@ /**

patchInstance: function(instance, Tools, Instance, Context, componentShared) {
// bind all instance methods to instance
// add Instance map

@@ -28,9 +30,11 @@ Object.defineProperty(instance, 'Instance', {

// add Shared object
Object.defineProperty(instance, componentShared._def.FullName, {
value: componentShared
});
Object.defineProperty(instance, 'Shared', {
value: componentShared
});
if (componentShared) {
// add Shared object
Object.defineProperty(instance, componentShared._def.FullName, {
value: componentShared
});
Object.defineProperty(instance, 'Shared', {
value: componentShared
});
}
},

@@ -67,3 +71,3 @@ };

*/
createInstanceMap: function(sessionId) {
getOrCreateInstanceMap: function(sessionId) {
// get or create instance map

@@ -77,10 +81,4 @@ // TODO: Make sure to set a max size on the cache and dequeue/destroy old instances.

// TODO: Make sure to get rid of unused instance objects
this.allInstances[sessionId] = Instance = Shared.Libs.ComponentDef.createComponentMap(false);
this.allInstances[sessionId] = Instance = Shared.Libs.ComponentDef._createComponentMap(false);
// create new Tools object that provides some shared methods for all component instance objects
var Tools = Shared.Libs.ComponentTools.createInstanceTools(Instance, Context);
// create new Context object that provides shared data across this component instanciation
var Context = Shared.Libs.ComponentContext.createInstanceContext(Instance);
// create new instance for the given component

@@ -92,3 +90,3 @@ var createInstance = function(componentShared) {

// create component instance object
// create component instance object and bind all methods to itself
var instance = Object.create(def.InstanceProto);

@@ -103,3 +101,9 @@

this.patchInstance(instance, Tools, Instance, Context, componentShared);
// add Shared object
Object.defineProperty(instance, def.FullName, {
value: componentShared
});
Object.defineProperty(instance, 'Shared', {
value: componentShared
});

@@ -111,2 +115,6 @@ // add to Instance collections

var patchInstance = function(instance) {
this.patchInstance(instance, Tools, Instance, Context);
}.bind(this);

@@ -117,5 +125,13 @@ // create lib instances

// create all other instances
// Tools et al are now ready and available.
var Tools = Shared.Libs.ComponentTools.createInstanceTools(Instance, Context);
var Context = Shared.Libs.ComponentContext.createInstanceContext(Instance);
// patch!
instanceMap.forEach(patchInstance);
// create and patch all other instances
instanceMap = Instance;
Shared.forEach(createInstance);
instanceMap.forEach(patchInstance);

@@ -154,6 +170,16 @@ // add a `client` object to each instance

activateSession: function(session, sessionId) {
var Instance = this.getInstanceMap(sessionId);
if (!Instance) return null;
activateSession: function(session, sessionId, force) {
var Instance;
if (force) {
Instance = this.getOrCreateInstanceMap(sessionId);
}
else {
Instance = this.getInstanceMap(sessionId);
if (!Instance) return null;
}
// update last used time
Instance.Libs.ComponentContext.touch();
// set session data
Instance.Libs.ComponentSession.setSession(session, sessionId);

@@ -175,11 +201,11 @@ return Instance;

onComponentInstallation: function(component) {
// patch up the client-side component instance:
this.patchInstance(component, Tools, Instance, Context, component);
// merge Private/Public into the Client instance
var privateName = component._def.getFullInstanceMemberName('Private/Public');
Instance.Libs.ComponentDef.doMerge(component, privateName, component._def.FullName + '.Client',
component = Instance.Libs.ComponentDef._doMergeEndpoints(component, privateName, component._def.FullName + '.Client',
component._def.InstanceProto, component, true);
// patch up the client-side component instance:
this.patchInstance(component, Tools, Instance, Context, component);
// call instance ctors

@@ -186,0 +212,0 @@ if (component.__baseCtor) {

@@ -10,2 +10,3 @@ /**

var fs = require('fs');
var process = require('process');

@@ -31,3 +32,3 @@ var ComponentDef = require('./ComponentDef');

var ComponentLoader = ComponentDef.lib({
Base: ComponentDef.defBase(function(SharedTools, Shared) {
Base: ComponentDef.defBase(function(SharedTools, Shared, SharedContext) {

@@ -40,3 +41,3 @@ return {

Host: ComponentDef.defHost(function(SharedTools, Shared) {
Host: ComponentDef.defHost(function(SharedTools, Shared, SharedContext) {

@@ -83,10 +84,15 @@ return {

ComponentCommunications.setupCommunications(app, cfg.endpointImplementation);
// call `initHost` on every registered component's Host endpoint
ComponentDef.initializeHostComponents(app, cfg);
// tell the bootstrapper to open for business
ComponentBootstrap.bootstrap(app, cfg);
return ComponentDef._initializeHostComponentsAsync(app, cfg)
.then(function() {
// tell the bootstrapper to open for business
ComponentBootstrap.bootstrap(app, cfg);
return Shared;
// return Shared component collection
return Shared;
})
.catch(function(err) {
process.exit('NoGap initialization failed: ' + (err && err.stack || err));
});
},

@@ -93,0 +99,0 @@

@@ -10,4 +10,153 @@ /**

module.exports = ComponentDef.lib({
Base: ComponentDef.defBase(function(SharedTools, Shared, SharedContext) {
/**
* Default error handler implementation
*/
var _onError = function(Instance, err, message) {
//var isException = err && err.stack;
var errString = err && err.stack || err;
var msg = (message && (message + ' - ') || '') + errString;
console.error(msg);
};
return {
/**
* This must be called from different stages of the intialization process in Client and Host!
*/
_initSharedTools: function() {
// add Promise library from ComponentDef
this.Promise = Shared.Libs.ComponentDef.Promise;
// bind all methods
this.bindAllMethodsToObject(this);
// merge this into `SharedTools` object
squishy.mergeWithoutOverride(SharedTools, this);
},
/**
* Bind all member functions of an object to the object itself.
*/
bindAllMethodsToObject: function(obj) {
for (var memberName in obj) {
if (!obj.hasOwnProperty(memberName)) continue;
var member = obj[memberName];
if (member instanceof Function) {
obj[memberName] = member.bind(obj);
}
}
},
Private: {
TraceCfg: {
enabled: true,
maxArgsLength: 120
},
getUserIdentifierImpl: function() {
// ask communication layer for an identifier
var userId = this.Instance.Libs.ComponentCommunications.getUserIdentifier();
return userId;
},
onError: function(err, message) {
_onError(this, err, message);
},
requestClientComponents: function(componentNames) {
if (!(componentNames instanceof Array)) {
componentNames = Array.prototype.slice.call(arguments, 0); // convert arguments to array
}
return this.Instance.Libs.ComponentBootstrap.requestClientComponents(componentNames);
},
/**
* Tell client to refresh current page.
*/
refresh: function() {
this.Instance.Libs.ComponentCommunications.refresh();
},
// ############################################################################################################
// User-specific logging
formatUserMessage: function(message) {
var userId = this.getUserIdentifierImpl();
var prefix = '';
if (userId) {
prefix = '[' + userId + '] ';
}
return prefix + message;
},
/**
*
*/
log: function(message) {
message = this.formatUserMessage(message);
console.log(message);
},
logWarn: function() {
message = this.formatUserMessage(message);
console.warn(message);
},
/**
* Default error handler.
* Can be overwritten.
*/
handleError: function(err, message) {
try {
this.onError(err, message);
}
catch (newErr) {
// error handling -> Make sure it always works or we simply won't see what's really happening!
console.error('Logging failed: ' + newErr && newErr.stack || newErr);
_onError(this.Instance, new Error(err && err.stack || err));
}
},
// ############################################################################################################
// User-specific tracing
traceLog: function(message) {
if (!this.TraceCfg) return;
this.log('[TRACE] ' + message);
},
traceFunctionCall: function(functionName, args) {
if (!this.TraceCfg) return;
var argsString = args ? JSON.stringify(args) : '';
if (argsString.length > this.TraceCfg.maxArgsLength) {
argsString = argsString.substring(0, this.TraceCfg.maxArgsLength) + '...';
}
this.traceLog('calling ' + functionName + '(' + argsString + ')');
},
traceComponentFunctionCall: function(componentName, functionName, args) {
if (!this.TraceCfg) return;
this.traceFunctionCall(componentName + '.' + functionName, args);
},
traceComponentFunctionCallFromSource: function(source, componentName, functionName, args) {
if (!this.TraceCfg) return;
this.traceFunctionCall('[from ' + source + '] ' + componentName + '.' + functionName, args);
},
}
};
}),
Host: ComponentDef.defHost(function(SharedTools, Shared, SharedContext) {
return {
__ctor: function() {
this._initSharedTools();
},
initHost: function() {

@@ -20,34 +169,15 @@ // add some shared tools

*/
createInstanceTools: function(Instance, Context) {
return {
requestClientComponents: function(componentNames) {
if (!(componentNames instanceof Array)) {
componentNames = Array.prototype.slice.call(arguments, 0); // convert arguments to array
}
Instance.Libs.ComponentBootstrap.requestClientComponentsFromHost(componentNames);
},
createInstanceTools: function(Instance) {
// use an UGLY hack for now
// TODO: This sort of initilization is necessary so the Tools instance is ready before we start
// getting into the dirty details of it all.
// BUT! it leaves the Tools object in an unfinished and totally broken state... o_X
//var tools = Object.create(this._def.InstanceProto);
// tools.Instance = Instance;
// tools.Shared = this;
/**
* Keeps buffering even after the current call ended.
* This is to signal the beginning of an asynchronous operation whose result is to be sent to the client.
*/
keepOpen: function() {
Instance.Libs.ComponentCommunications.keepOpen();
},
var tools = Instance.Libs.ComponentTools;
SharedTools.bindAllMethodsToObject(tools); // bind all methods
/**
* Flushes the current buffer.
* This is to signal the end of an asynchronous operation whose result has already been sent to the client.
*/
flush: function() {
Instance.Libs.ComponentCommunications.flush();
},
/**
* Tell client to refresh current page.
*/
refresh: function() {
Instance.Libs.ComponentBootstrap.refresh();
}
};
return tools;
},

@@ -63,20 +193,20 @@

return {
Private: {
initClient: function() {
// add some client tools
/**
* Ask server for the given additional components.
*/
Tools.requestClientComponents = function(namessss) {
Instance.Libs.ComponentBootstrap.requestClientComponents.apply(Instance.Libs.ComponentBootstrap, arguments);
};
/**
* Refresh current page.
*/
Tools.refresh = Instance.Libs.ComponentBootstrap.refresh.bind(Instance.Libs.ComponentBootstrap);
},
initClient: function() {
this.initClient = null;
},
/**
* Refresh current page.
*/
refresh: function() {
Instance.Libs.ComponentBootstrap.refresh();
},
Private: {
__ctor: function() {
this._initSharedTools();
}
},
/**

@@ -83,0 +213,0 @@ * Client commands can be directly called by the host

{
"name": "nogap",
"version": "0.4.4",
"version": "0.5.0",
"author": {

@@ -19,3 +19,3 @@ "name": "Dominik Seifert",

"readmeFilename": "README.md",
"description": "RPC + improved code sharing + asset management + some other good stuff for enjoyable Host <-> Client architecture development.",
"description": "NoGap is a full-stack (spans Host and Client) JavaScript framework, featuring RPC + simple code sharing + basic asset management + full-stack Promise chains and more...",

@@ -22,0 +22,0 @@ "bugs": {

@@ -6,8 +6,7 @@ [![NPM version](https://badge.fury.io/js/nogap.svg)](http://badge.fury.io/js/nogap)

The NoGap framework delivers [RPC (Remote Procedure Call)](http://en.wikipedia.org/wiki/Remote_procedure_call) + improved code sharing + asset management + some other good stuff for enjoyable Host &lt;-> Client architecture development.
NoGap is a full-stack (spans Host and Client) JavaScript framework, featuring [RPC (Remote Procedure Calls)](http://en.wikipedia.org/wiki/Remote_procedure_call) + simple code sharing + basic asset management + full-stack [Promise chains](https://github.com/petkaantonov/bluebird#what-are-promises-and-why-should-i-use-them).
NoGap's primary use case is development of rich single-page, client-side applications while alleviating the typical hassles of doing so.
NoGap's primary use case is development of rich single-page web applications while alleviating the typical hassles of doing so.
This module is called `No` `Gap` because it removes the typical gap that exists between
host and client and that makes a Client <-> Server architecture so cumbersome to develop.
This module is called `No` `Gap` because it removes the typical gap that exists between Host and Client and that makes a client-server-architecture so cumbersome to develop.

@@ -18,3 +17,3 @@ You probably want to start by having a look at the [Samples](#samples) for reference.

When starting on a new component, you can save a bit of time by copying the [typical component skeleton code](#component_skeleton) from the [Structure of NoGap components](#component_structure) section.
The [Structure of NoGap components](#component_structure) section lays out the structure of NoGap's basic building block: the component.

@@ -39,2 +38,3 @@ Note that currently, the only dependency of NoGap is `Node` and some of its modules but even that is planned to be removed in the future.

* [TwoWayStreet](#twowaystreet)
* [Full-stack promise chains](#full-stack-promise-chains)
* [TwoWayStreetAsync](#twowaystreetasync)

@@ -44,4 +44,4 @@ * [CodeSharingValidation](#codesharingvalidation)

* [Multiple Components](#multiple-components)
* [Full-stack error handling](#full-stack-error-handling)
* [Dynamic Loading of Components](#dynamic-loading-of-components)
* [Request &lt;-> Reply Pairs](#request-lt-reply-pairs)
* [Simple Sample App](#simple-sample-app)

@@ -164,8 +164,9 @@ * [Component Structure](#component-structure)

Host: NoGapDef.defHost(function(SharedTools, Shared, SharedContext) {
var iAttempt = 0;
var nBytes = 0;
return {
Public: {
tellClientSomething: function(sender) {
this.client.showHostMessage('We have exchanged ' + ++iAttempt + ' messages.');
tellMeSomething: function(message) {
nBytes += (message && message.length) || 0;
this.client.showHostMessage('Host has received a total of ' + nBytes + ' bytes.');
}

@@ -179,10 +180,12 @@ }

initClient: function() {
window.clickMe = function() {
document.body.innerHTML +='Button was clicked.<br />';
this.host.tellClientSomething();
}.bind(this);
// bind a button to a component function (quick + dirty):
window.clickMe = this.onButtonClick.bind(this);
document.body.innerHTML += '<button onclick="window.clickMe();">Click Me!</button><br />';
},
onButtonClick: function() {
document.body.innerHTML +='Button was clicked.<br />';
this.host.tellMeSomething('hello!');
},
Public: {

@@ -207,3 +210,3 @@ showHostMessage: function(msg) {

* `this.host` gives us an object on which we can call `Public` methods on the host
* For example, we can call `tellClientSomething` which is a method that was defined in `Host.Public`
* For example, we can call `tellMeSomething` which is a method that was defined in `Host.Public`
* Once the host receives our request, it calls `this.client.showHostMessage`

@@ -213,28 +216,80 @@ * Note: `this.host` (available on Client) vs. `this.client` (available on Host)

## Full-stack promise chains
<!-- [Link](samples/). -->
NoGap supports full-stack [Promise chains](https://github.com/petkaantonov/bluebird#what-are-promises-and-why-should-i-use-them). Meaning you can let the Client wait until a Host-side function call has returned. And you can even return a value from a Host function, and it will arrive at the Client. Errors also traverse the entire stack!
Code snippet:
```js
tellMeSomething: function(name) {
nBytes += (message && message.length) || 0;
return 'Host has received a total of ' + nBytes + ' bytes.';
}
// ...
onButtonClick: function() {
document.body.innerHTML +='Button was clicked.<br />';
this.host.tellMeSomething('hello!')
.bind(this) // this is tricky!
.then(function(hostMessage) {
this.showHostMessage(hostMessage);
});
},
```
**New Concepts**
* Calling a `Public` function on a component's `host` object returns a promise.
* That promise is part of a full-stack [Promise chains](https://github.com/petkaantonov/bluebird#what-are-promises-and-why-should-i-use-them). A value returned by a `Host`'s `Public` function (or by a promise returned by such function), will be received by the client.
* Note that [JavaScript's `this` is tricky](http://javascriptissexy.com/understand-javascripts-this-with-clarity-and-master-it/)!
## TwoWayStreetAsync
[Link](samples/TwoWayStreetAsync).
Now that our code keeps growing and you are starting to get the picture, let us just focus on code snippets from now on.
Imagine the server had to do an [asynchronous operation](http://msdn.microsoft.com/en-us/library/windows/apps/hh700330.aspx) in [`tellMeSomething`](#twowaystreet), such as reading a file, or getting something from the database.
Imagine the server had to do an asynchronous operation in [`tellClientSomething`](#twowaystreet).
For example, it needs to read a file, or get something from the database.
We can simply use promises for that!
```js
tellClientSomething: function() {
this.Tools.keepOpen();
// wait 500 milliseconds before replying
setTimeout(function() {
tellMeSomething: function() {
Promise.delay(500) // wait 500 milliseconds before replying
.bind(this) // this is tricky!
.then(function() {
this.client.showHostMessage('We have exchanged ' + ++iAttempt + ' messages.');
this.Tools.flush();
}.bind(this), 500);
});
}
```
And again, we can just return the message and it will arrive at the Client automagically, like so:
```js
tellMeSomething: function() {
Promise.delay(500) // wait 500 milliseconds before replying
.bind(this) // this is tricky!
.then(function() {
return 'We have exchanged ' + ++iAttempt + ' messages.';
});
}
// ...
onButtonClick: function() {
document.body.innerHTML +='Button was clicked.<br />';
this.host.tellMeSomething()
.bind(this) // this is tricky!
.then(function(hostMessage) {
this.showHostMessage(hostMessage);
});
},
```
**New Concepts**
* We need to perform an asynchronous request whose result is to be sent to the other side:
* In that case, first call `this.Tools.keepOpen()`, so the client connection will not be closed automatically
* Once you sent everything to the client, call `this.Tools.flush()`
* We need to perform an asynchronous request whose result is to be sent to the other side
* Simply use [Promise chains](https://github.com/petkaantonov/bluebird#what-are-promises-and-why-should-i-use-them)!
## CodeSharingValidation

@@ -256,3 +311,3 @@ [Link](samples/CodeSharingValidation).

Public: {
setValue: function(sender, value) {
setValue: function(value) {
this.value = this.Shared.validateText(value);

@@ -325,3 +380,2 @@ // ...

## Multiple Components

@@ -336,3 +390,10 @@

## Full-stack error handling
TODO!
* Feel free to try and throw an error or use `Promise.reject` in a `Host`'s `Public` function, and then `catch` it on the Client side. You will notice that, for security reasons, the contents of Host-side exceptions are modified before being sent to the Client.
* You can override `Tools.onError` to customize error handling (especially on the server)
* TODO: Trace verbosity configuration
## Dynamic Loading of Components

@@ -348,40 +409,2 @@ <!-- [Link](samples/DynamicLoading). -->

## Request &lt;-> Reply Pairs
<!-- [Link](samples/). -->
Code snippet:
Host: {
Public: {
myStuff: [...],
checkIn: function(sender, name) {
// call Client's `onReply` callback
sender.reply('Thank you, ' + name + '!', myStuff);
}
}
}
// ...
Client: {
// ...
initClient: {
// call function on Host, then wait for Host to reply
this.host.checkIn('Average Joe')
.onReply(function(message, stuff) {
// server sent something back
// ...
});
}
}
**Concepts**
* When calling a `Host.Public` method (e.g. `checkIn`), in addition to the arguments sent by the client, there is an argument injected before all the others, called `sender`.
* When calling a `Host.Public` method, you can register a callback by calling `onReply` (e.g. `checkIn(...).onReply(function(...) { ... }`).
* The `Host` can then call `sender.reply` which will lead to the `onReply` callback to be called.
## Simple Sample App

@@ -405,13 +428,13 @@ [Link](samples/sample_app).

1. The **shared object** of a component exists only once for the entire application. It is what is returned if you `require` the component file in Node. You can access all of shared component objects through the `Shared` set which is the second argument of every `Host`'s *component definition*.
1. The **Shared object** of a component is a singleton; it exists only once for the entire application. You can access all `Shared` component objects through the `Shared` set which is the second argument of every `Host`'s *component definition*.
2. The **instance object** of a component exists once for every client. Every client that connects to the server, gets its own set of instances of every active component. On the `Host` side, the *instance object* of a component is defined as the merged result of all members of `Private` and `Public` which we call *instance members*. These instance members are accessible through `this.Instance` from **instance code**, that is code inside of `Private` and `Public` properties. If you want to hook into client connection and component bootstrapping events, simply defined `onNewClient` or `onClientBootstrap` functions inside `Host.Private`. You can access the respective *shared members* through `this.Shared` from *instance code*.
Inside a `Host` instance object, you can directly call `Public` instance members on the client through `this.client.someClientPublicMethod(some, data)`. Being able to directly call a function on a different computer or in a different program is called [RPC (Remote Procedure Calls)](http://en.wikipedia.org/wiki/Remote_procedure_call). Similarly, `Client` instances can directly call `this.host.someHostPublicMethod`. Note that when you call `Host.Public` methods, an argument gets injected before all other arguments, called the `sender`. The `sender` argument gives context sensitive information on where the call originated from and can be used for simple request &lt;-> **reply** pairs, and for debugging purposes.
2. The **instance object** of a component exists once for every client. Every client that connects to the server, gets its own set of instances of every active component. On the `Host` side, the *instance object* of a component is defined as the merged result of all members of `Private` and `Public` which we call *instance members*. These instance members are accessible through `this.Instance` from **instance code**, that is, code inside of `Private` and `Public` properties. If you want to hook into client connection and component bootstrapping events, simply define `onNewClient` or `onClientBootstrap` functions inside `Host.Private`. You can access the owning component's *Shared singleton* through `this.Shared` from within `Private` or `Public` functions.
Inside a `Host` instance object, you can directly call `Public` instance members on the client through `this.client.someClientPublicMethod(some, data)`. Being able to directly call a function on a different computer or in a different program is called [RPC (Remote Procedure Call)](http://en.wikipedia.org/wiki/Remote_procedure_call). Similarly, `Client` instances can directly call `this.host.someHostPublicMethod` which returns a [Promise](https://github.com/petkaantonov/bluebird#what-are-promises-and-why-should-i-use-them) which will be fulfilled once the `Host` has run the function and notified the client.
## `Client`
The set of all `Client` endpoint definition is automatically sent to the client and installed, as soon a client connects. On the client side, `this.Shared` and `this.Instance` refer to the same object, and `Private` and `Public` are both merged into the `Client` *component definition* itself. If you want to load components dynamically (or lazily; `lazyLoad` is set to 1), during certain events, you need to set the `lazyLoad` config parameter to `true` or `1`.
The set of all `Client` endpoint definitions is automatically sent to the client and installed, as soon as a client connects. On the client side, `this.Shared` and `this.Instance` refer to the same object, and `Private` and `Public` are both merged into the `Client` *component definition* itself. If you want to load components dynamically (i.e. lazily), you need to set the `lazyLoad` config parameter to `true` or `1`.
## `Base`
Everything from the `Base` definition is merged into both, `Host` and `Client`. `Public` and `Private` are also merged correspondingly. Since `Host` and `Client` operate slightly different, certain naming decisions had to be made seemingly in favor of one over the other. E.g. the `Shared` concept does not exist on client side (because a `Client` only contains a single instance of all components), so there, it simply is the same as `Instance`.
Inside `Base` members, you can call `this.someMethod` even if `someMethod` is not declared in `Base`, but instead is declared in `Host` as well as `Client`. At the same time, you can call `this.someBaseMethod` from each endpoint definition. That enables you to easily have shared code call endpoint-specific code and vice versa, thereby supporting polymorphism and encapsulation.
Inside `Base` members, you can call `this.someMethod` even if `someMethod` is not declared in `Base`, but instead is declared in `Host` as well as `Client`. At the same time, you can call `this.someBaseMethod` from `Client` or `Host`. That enables you to easily have shared code call endpoint-specific code and vice versa, thereby supporting polymorphism and encapsulation.

@@ -421,3 +444,3 @@

<a name="component_skeleton"></a>
This skeleton code summarizes (most of) available component structure:
This skeleton code summarizes (most of) the available component structure:

@@ -488,3 +511,3 @@

* The ctor is called only once, during NoGap initialization,
* when the shared component part is created.
* when the `Shared` component part is created.
* Will be removed once called.

@@ -497,3 +520,3 @@ */

* Is called once on each component after
* all components have been created.
* all components have been created, and after `initBase`.
*/

@@ -523,4 +546,4 @@ initHost: function() {

* Called after `onNewClient`, once this component
* is bootstrapped on the client side.
* Since components can be deployed dynamically,
* is about to be sent to the `Client`.
* Since components can be deployed dynamically (if `lazyLoad` is enabled),
* this might happen much later, or never.

@@ -554,4 +577,3 @@ */

* Called once after all currently deployed client-side
* components have been created.
* Will be removed once called.
* components have been created, and after `initBase`.
*/

@@ -563,4 +585,5 @@ initClient: function() {

/**
* Called after the given component has been loaded in the client.
* NOTE: This is important when components are dynamically loaded (`lazyLoad` = 1).
* Called after the given component has been loaded in the Client.
* NOTE: This is generally only important when components are dynamically loaded (`lazyLoad` = 1).
* (Because else, `initClient` will do the trick.)
*/

@@ -572,6 +595,7 @@ onNewComponent: function(newComponent) {

/**
* Called after the given batch of components has been loaded in the client.
* Called after the given batch of components has been loaded in the Client.
* This is called after `onNewComponent` has been called
* on each individual component.
* NOTE: This is important when components are dynamically loaded (`lazyLoad` = 1).
* NOTE: This is generally only important when components are dynamically loaded (`lazyLoad` = 1).
* (Because else, `initClient` will do the trick.)
*/

@@ -583,4 +607,4 @@ onNewComponents: function(newComponents) {

/**
* This is optional and will be merged into the Client instance,
* residing along-side the members defined above.
* This will be merged into the Client instance.
* It's members will reside along-side the members defined above it.
*/

@@ -605,2 +629,4 @@ Private: {

TODO: Need to rewrite this with to work with the new version that adapted full-stack Promises.
This tutorial is aimed at those who are new to `NoGap`, and new to `Node` in general.

@@ -685,6 +711,3 @@ It should help you bridge the gap from the [Code Snippets](#samples) to a real-world application.

* If you are interested into the dirty details, have a look at [`HttpPostImpl` in `ComponentCommunications.js`](https://github.com/Domiii/NoGap/blob/master/lib/ComponentCommunications.js#L564)
* `traceKeepOpen` (Default = 0)
* This is for debugging your `keepOpen` and `flush` pairs. If you don't pair them up correctly, the client might wait forever.
* If your client does not receive any data, try setting this value to 4 and check if all calls pair up correctly.
* The value determines how many lines of stacktrace to show, relative to the first non-internal call; that is the first stackframe whose code is not located in the NoGap folder.
* TODO: Tracing, logging + customized error handling

@@ -734,3 +757,3 @@

=============
By default, each `Client` only receives code from `Client` and `Base` definitions. `Host`-only code is not available to the client. However, the names of absolute file paths are sent to the client to facilitate perfect debugging; i.e. all stacktraces and the debugger will refer to the correct line inside the actual host-resident component file. If that is of concern to you, let me know, and I'll move up TODO priority of name scrambling, or have a look at [`ComponentDef`'s `FactoryDef`, and the corresponding `def*` methods](https://github.com/Domiii/NoGap/blob/master/lib/ComponentDef.js#L71) yourself.
By default, each `Client` only receives `Client` and `Base` definitions. `Host`-only code is not available to the client. However, the names of absolute file paths are sent to the client to facilitate perfect debugging; i.e. all stacktraces and the debugger will refer to the correct line inside the actual host-resident component file. If that is of concern to you, let me know, and I'll move up TODO priority of name scrambling, or have a look at [`ComponentDef`'s `FactoryDef`, and the corresponding `def*` methods](https://github.com/Domiii/NoGap/blob/master/lib/ComponentDef.js#L71) yourself.

@@ -740,3 +763,3 @@

=============
TODO: Add more links + terms.
TODO: Add links + more terms.

@@ -746,9 +769,8 @@ * Component

* Client
* Base (mergd into Client and Host)
* Instance (set of all component instance objects)
* Shared (set of all component shared objects)
* Endpoint (refers to Client or Host)
* Base (merged into Client and Host)
* Shared (set of all component singletons)
* Instance (set of all component instance objects, exist each once per connected client)
* Tools (set of functions to assist managing of components)
* Context
* Asset (an asset is content data, such as html and css code, images and more)
* Asset (an asset is content data, such as HTML and CSS code, images and more)
* more...

@@ -760,2 +782,2 @@

Good luck! In case of questions, feel free to contact me.
Good luck! In case of any questions, feel free to contact me.
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc