nogap
Advanced tools
Comparing version 0.4.4 to 0.5.0
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 <-> 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 <-> **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": { |
218
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. | ||
@@ -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 <-> 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 <-> 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 <-> **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. |
Uses eval
Supply chain riskPackage uses eval() which is a dangerous function. This prevents the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
349646
44
4763
757
2
9
6