├── .eslintrc ├── .gitignore ├── .versions ├── CHANGELOG.md ├── README.md ├── circle.yml ├── git-packages.json ├── package.js ├── source ├── api.coffee ├── command_bus.coffee ├── controller.js ├── event_bus.coffee ├── helpers.js ├── mixins │ ├── application-helpers.coffee │ ├── command-handling.js │ ├── command-sending.coffee │ ├── declarative-mappings.js │ ├── ejsonable.js │ ├── event-publishing.coffee │ ├── event-subscribing.js │ ├── static-handlers.coffee │ └── versionable.js ├── module.coffee ├── publication.coffee ├── serializables │ ├── command.js │ ├── error.js │ └── event.js ├── tracker.coffee └── value-objects │ └── guid.coffee ├── test.sh ├── tests ├── integration │ ├── controller_command_handling.js │ ├── controller_event_subscribing.js │ ├── handling-api-messages.js │ └── test-app.js └── unit │ ├── api.unit.coffee │ ├── command_bus.unit.coffee │ ├── event_bus.unit.coffee │ ├── helpers.tests.js │ ├── mixins │ ├── command-handling.tests.js │ ├── event-subscribing.tests.js │ └── versionable.tests.js │ ├── serializable.unit.coffee │ ├── serializables │ ├── command.unit.coffee │ ├── event.unit.coffee │ └── space-error.tests.js │ └── value-objects │ └── guid.unit.coffee └── versions.json /.eslintrc: -------------------------------------------------------------------------------- 1 | /** 2 | * 0 - turn the rule off 3 | * 1 - turn the rule on as a warning (doesn't affect exit code) 4 | * 2 - turn the rule on as an error (exit code will be 1) 5 | * 6 | * Meteor Style Guide: https://github.com/meteor/meteor/wiki/Meteor-Style-Guide 7 | * 8 | */ 9 | 10 | { 11 | "parser": "babel-eslint", 12 | "env": { 13 | "browser": true, 14 | "node": true 15 | }, 16 | "ecmaFeatures": { 17 | "arrowFunctions": true, 18 | "blockBindings": true, 19 | "classes": true, 20 | "defaultParams": true, 21 | "destructuring": true, 22 | "forOf": true, 23 | "generators": false, 24 | "modules": true, 25 | "objectLiteralComputedProperties": true, 26 | "objectLiteralDuplicateProperties": false, 27 | "objectLiteralShorthandMethods": true, 28 | "objectLiteralShorthandProperties": true, 29 | "spread": true, 30 | "superInFunctions": true, 31 | "templateStrings": true, 32 | "jsx": true 33 | }, 34 | "rules": { 35 | /** 36 | * Strict mode 37 | */ 38 | // babel inserts "use strict"; for us 39 | // http://eslint.org/docs/rules/strict 40 | "strict": 0, 41 | 42 | /** 43 | * ES6 44 | */ 45 | "no-var": 1, // http://eslint.org/docs/rules/no-var 46 | 47 | /** 48 | * Variables 49 | */ 50 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 51 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 52 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars 53 | "vars": "local", 54 | "args": "after-used" 55 | }], 56 | "no-use-before-define": [2, "nofunc"], // http://eslint.org/docs/rules/no-use-before-define 57 | 58 | /** 59 | * Possible errors 60 | */ 61 | "comma-dangle": [1, "never"], // http://eslint.org/docs/rules/comma-dangle 62 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign 63 | "no-console": 1, // http://eslint.org/docs/rules/no-console 64 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 65 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 66 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 67 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 68 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 69 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 70 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 71 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 72 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 73 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 74 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 75 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 76 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 77 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 78 | "quote-props": [2, "as-needed", { "keywords": true, "unnecessary": false }], // http://eslint.org/docs/rules/quote-props (previously known as no-reserved-keys) 79 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 80 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 81 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 82 | "block-scoped-var": 0, // http://eslint.org/docs/rules/block-scoped-var 83 | 84 | /** 85 | * Best practices 86 | */ 87 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 88 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly 89 | "default-case": 2, // http://eslint.org/docs/rules/default-case 90 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation 91 | "allowKeywords": true 92 | }], 93 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 94 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in 95 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 96 | //"no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 97 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 98 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 99 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 100 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 101 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 102 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 103 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 104 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 105 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 106 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 107 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 108 | "no-new": 2, // http://eslint.org/docs/rules/no-new 109 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 110 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 111 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 112 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 113 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign 114 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 115 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 116 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 117 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 118 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 119 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 120 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 121 | "no-with": 2, // http://eslint.org/docs/rules/no-with 122 | "radix": 2, // http://eslint.org/docs/rules/radix 123 | "vars-on-top": 1, // http://eslint.org/docs/rules/vars-on-top 124 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 125 | "yoda": 2, // http://eslint.org/docs/rules/yoda 126 | "max-len": [1, 200, 2], // http://eslint.org/docs/rules/max-len 127 | 128 | /** 129 | * Style 130 | */ 131 | "indent": [2, 2, {"VariableDeclarator": 2}], // http://eslint.org/docs/rules/indent 132 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style 133 | "1tbs", { 134 | "allowSingleLine": true 135 | }], 136 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 137 | "properties": "never" 138 | }], 139 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 140 | "before": false, 141 | "after": true 142 | }], 143 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 144 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 145 | "func-names": 0, // http://eslint.org/docs/rules/func-names 146 | "func-style": [2, "expression"], // http://eslint.org/docs/rules/func-style 147 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 148 | "beforeColon": false, 149 | "afterColon": true 150 | }], 151 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap 152 | "newIsCap": true, 153 | "capIsNew": false 154 | }], 155 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 156 | "max": 2 157 | }], 158 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 159 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 160 | "no-array-constructor": 2, // http://eslint.org/docs/rules/no-array-constructor 161 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 162 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 163 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 164 | "one-var": [1, "never"], // http://eslint.org/docs/rules/one-var 165 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi 166 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing 167 | "before": false, 168 | "after": true 169 | }], 170 | "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords 171 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 172 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren 173 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 174 | "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case 175 | "spaced-comment": 2, // http://eslint.org/docs/rules/spaced-comment (previously known as spaced-line-comment) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | local-packages.json 3 | packages 4 | .idea -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@5.8.24_1 2 | babel-runtime@0.1.4 3 | base64@1.0.4 4 | binary-heap@1.0.4 5 | blaze@2.1.3 6 | blaze-tools@1.0.4 7 | boilerplate-generator@1.0.4 8 | caching-compiler@1.0.0 9 | callback-hook@1.0.4 10 | check@1.1.0 11 | coffeescript@1.0.11 12 | ddp@1.2.2 13 | ddp-client@1.2.1 14 | ddp-common@1.2.2 15 | ddp-server@1.2.2 16 | deps@1.0.9 17 | diff-sequence@1.0.1 18 | ecmascript@0.1.6 19 | ecmascript-runtime@0.2.6 20 | ejson@1.0.7 21 | geojson-utils@1.0.4 22 | html-tools@1.0.5 23 | htmljs@1.0.5 24 | id-map@1.0.4 25 | jquery@1.11.4 26 | local-test:space:messaging@3.3.1 27 | logging@1.0.8 28 | meteor@1.1.10 29 | minimongo@1.0.10 30 | mongo@1.1.3 31 | mongo-id@1.0.1 32 | npm-mongo@1.4.39_1 33 | observe-sequence@1.0.7 34 | ordered-dict@1.0.4 35 | practicalmeteor:chai@2.1.0_1 36 | practicalmeteor:loglevel@1.2.0_2 37 | practicalmeteor:munit@2.1.5 38 | practicalmeteor:sinon@1.14.1_2 39 | promise@0.5.1 40 | random@1.0.5 41 | reactive-dict@1.1.3 42 | reactive-var@1.0.6 43 | retry@1.0.4 44 | routepolicy@1.0.6 45 | space:base@4.1.3 46 | space:messaging@3.3.1 47 | space:testing@3.0.2 48 | space:testing-messaging@3.0.1 49 | spacebars@1.0.7 50 | spacebars-compiler@1.0.7 51 | test-helpers@1.0.5 52 | tinytest@1.0.6 53 | tracker@1.0.9 54 | ui@1.0.8 55 | underscore@1.0.4 56 | webapp@1.2.3 57 | webapp-hashing@1.0.5 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | ## 3.3.1 4 | ### Bug fixes 5 | - The callback passed to `Space.messaging.CommandBus.send` is now mapped through to the server-side command handler. Before this change it was only being passed through on the client. 6 | - Removed an unused dependency fongandrew:find-and-modify 7 | 8 | ## 3.3.0 9 | ### New features 10 | - `Space.messaging.define` helper returns object with defined serializables which makes helper more usable when using ES6 module imports/exports. 11 | Following implementation is now possible: 12 | 13 | ``` javascript 14 | // Defining events: 15 | export default Space.messaging.define(Space.domain.Event, { 16 | 17 | BankAccountOpened: { 18 | owner: Contact, 19 | balance: Money 20 | }, 21 | 22 | BankAccountCredited: { 23 | balance: Money 24 | } 25 | 26 | }); 27 | ``` 28 | 29 | ``` javascript 30 | // Importing and using events: 31 | import events from '../events'; 32 | const { BankAccountOpened } = events; 33 | 34 | let event = new BankAccountOpened({ 35 | sourceId: new Guid() 36 | // ... 37 | }); 38 | 39 | ``` 40 | 41 | - New method `getHandledCommandTypes()` on `Space.messaging.CommandBus` 42 | 43 | ## 3.2.0 44 | ### New Features 45 | - `Space.messaging.Command` is now versionable, allowing older versions to be 46 | transformed, primarily intended when deserializing from a persistent store. 47 | - `Space.Error` is also now versionable when `space:messaging` is installed. 48 | 49 | ## 3.1.1 50 | ### Bug fixes 51 | - Regression bug with following methods: 52 | - `Space.messaging.EventSubscribing.canHandleEvent` 53 | - `Space.messaging.EventSubscribing._getEventHandlerFor` 54 | - Invalid logic in onConstruction callbacks in: 55 | - `Space.messaging.EventSubscribing` 56 | - `Space.messaging.CommandHandling` 57 | 58 | ## 3.1.0 59 | - Introduces [`Space.messaging.Versionable`](https://github.com/meteor-space/messaging/blob/master/source/mixins/versionable.js) mixin to allow any `Space.Struct` to be migrated to new versions as defined by `schemaVersion` using a transformation function such as `transformFromVersionX(data)`. This is particularly important once an `Ejsonable` `Space.Struct` is persisted. 60 | - Any changes to the struct’s fields require the version to be incremented by first adding the property `schemaVersion`, which by default is set to 1, and then defining a transformation function. 61 | - Fixes the mixin definition of `EventSubscribing` and `CommandSending`, so that they do not override the handler methods on the host class if mixed in after class creation. 62 | 63 | ## 3.0.1 64 | - Removes default fields from `Space.messaging.Event` making the class more generalised and better suited for more specialized `Space.ui.Event` and `Space.domain.Event` classes.```` 65 | - Removes default fields from `Space.messaging.Command` making the class more generalised and better suited for more specialized `Space.domain.Command` class. 66 | 67 | ## 3.0.0 68 | ### New Features 69 | - Register a callback with a CommandBus or EventBus instance using `onSend(callback)` or `onPublish(callback)` respectively. Message will be passed in as the first parameter. 70 | 71 | ### Breaking changes 72 | - This version uses space:base 4.x which includes breaking changes. Please see the [changelog](https://github.com/meteor-space/base/blob/master/CHANGELOG.md). 73 | - Must be running Meteor 1.2.0.1 or later. 74 | - `Space.messaging.Event` and `Space.messaging.Command` no longer include 75 | default fields `sourceId`/`targetId`, `timestamp`, or `version`. 76 | These fields were added to support domain events and commands, 77 | but were superfluous for UI events. The drop-in replacement objects are available in [space:domain](https://github.com/meteor-space/domain) as `Space.domain.Event` and `Space.domain.Command`. 78 | - `Space.messaging.SerializableMixin` renamed to `Space.messaging.Ejsonable`. Best practice is to use this mixin and extend from `Space.Struct` as `Space.messaging.Serializable` will be depreciated in the future. 79 | 80 | ## 2.1.0 81 | This is a continuous breaking changes release because nobody is using 2.0.0 yet. 82 | - Improved naming of event subscribing / command handling api to `eventSubscriptions` 83 | and `commandHandlers` 84 | - Added mixin that simplifies the declaration of handlers with array maps. This 85 | is used in several classes / mixins now. 86 | - Adds declarative api to `Space.messaging.Api` and `Space.messaging.Publication` 87 | to make them more Javascript compatible. 88 | - Updates to `space:base@3.1.0` 89 | 90 | ## 2.0.0 91 | Cleanup and breaking changes release: 92 | - Updated to `space:base@3.0.0` with improved module lifecycle api 93 | - Removed distributed messaging api in favor of the distributed commit store 94 | in the `event-sourcing` package. 95 | - The package mixes in application helpers for publishing events and sending 96 | commands as well as subscribing etc. 97 | - The `Space.messaging.Controller` functionality was split up into four mixins 98 | `EventSubscribing`, `EventPublishing`, `CommandHandling` and `CommandSending` 99 | 100 | ## 1.8.0 101 | The following improvements have been made: 102 | - Adds `Space.messaging.EvenBus::onPublish` hook that is called for any event 103 | that goes through the system. Useful for testing or when you want to aggregate 104 | event statistics. 105 | - Event and command handling capabilities are now mixed into `Space.Application` 106 | by default. So you can use `publish`, `subscribeTo` for events and `send` commands. 107 | - Event `sourceId` and command `targetId` are now optional fields that match 108 | to `String` and `Guid` by default. So you only have to define these yourself 109 | in rare cases where you need extra control or something. 110 | 111 | ## 1.7.2 112 | Adds declarative API for defining event / command handlers in `Space.messaging.Controller`. 113 | Now you can define them like this: 114 | ```javascript 115 | Space.messaging.Controller.extend('MyController', { 116 | events: function() { 117 | return [{ 118 | 'TestApp.TestEvent': this._handleTestEvent, 119 | 'TestApp.AnotherEvent': this._anotherEventHandler 120 | }]; 121 | }, 122 | commands: function() { 123 | return [{ 124 | 'TestApp.TestCommand': this._handleTestCommand, 125 | 'TestApp.AnotherCommand': this._anotherCommandHandler 126 | }]; 127 | } 128 | }); 129 | ``` 130 | 131 | ## 1.7.1 132 | Adds small api helpers to `Space.messaging.Controller`: 133 | - Now you can ask if a controller can handle a certain message with `canHandleEvent` and `canHandleCommand` 134 | 135 | ## 1.7.0 136 | Adds versioning support for `Space.messaging.Event`. This makes it possible 137 | to have multiple versions of saved event data and dynamically migrate older 138 | versions to the latest required structure. Take a look at the unit tests to 139 | get a feeling how this works ;-) 140 | 141 | ## 1.6.1 142 | Updated location of Github repository to https://github.com/meteor-space/messaging 143 | 144 | ## 1.6.0 145 | - Improves API of `Space.messaging.define` for use in Javascript. 146 | - Let static methods like `Controller::handle` and `on` always return the class 147 | instance so that it can be chained (again more beautiful in Javascript). 148 | 149 | ## 1.5.2 150 | - Improves the way `Space.messaging.Serializable` objects are serialized from and 151 | to EJSON values. It uses `toJSONValue` and `fromJSONValue` instead of `stringify` 152 | and `parse` to tranform the fields now. 153 | 154 | ## 1.5.1 155 | - EJSON stringify and parse distributed events to avoid the weird transformations done by Meteor mongo driver. This makes integration 156 | with standard EJSON much easier, e.g when using the ejson npm package. 157 | 158 | ## 1.5.0 159 | - Adds first draft of a basic API for distributed events via a shared mongo 160 | collection that can be used to integrate separate apps. 161 | 162 | ## 1.4.2 163 | - Provide publish context as first argument like for api methods. 164 | 165 | ## 1.4.1 166 | - Adds the sugar methods `publish` and `send` to `Space.messaging.Controller` 167 | which forward the respective messages to the event and command busses. 168 | 169 | ## 1.4.0 170 | - Map `Space.messaging.Api` as static value 171 | - Allow to pass a callback for `Space.messaging.Api#send` as last param 172 | 173 | ## 1.3.2 174 | Adds `Space.messaging.Api.send` method which is a simple sugar wrapper around 175 | `Meteor.call` to send events and commands to the server. 176 | 177 | ## 1.3.1 178 | Moves `Space.messaging.Api` into shared environment so that you can also 179 | setup a method simulations like normal in Meteor. To check if it is a simulation 180 | just use the first context param where `isSimulation` should be defined. 181 | 182 | ## 1.3.0 183 | 184 | - Adds a very simple wrapper around `Tracker.autorun` called `Space.messaging.Tracker` 185 | which can be used in scenarios where you want to run code reactively in response 186 | to property changes. 187 | - Another sweet sugar around `Meteor.publish` called `Space.messaging.Publication` 188 | which allows to define `@publish 'my-name', (param1, param2, …) ->` publications 189 | which are run in the context of the class instance. 190 | 191 | ## 1.2.2 192 | 193 | - Improves error handling while defining event and command handlers in 194 | `Space.messaging.Controller` 195 | - Fixes problem with defining serializables 196 | - Allow null values to be serialized 197 | 198 | ## 1.2.1 199 | Introduces convenient API `Space.messaging.defineSerializables` which can be 200 | used to define any number of serializables at once without the boilerplate of 201 | class definitions. 202 | 203 | ## 1.1.0 204 | - Introduces (optional) typed methods for `Space.messaging.Api`. This makes it 205 | possible that the message type is automatically checked for you like this: 206 | 207 | ```coffeescript 208 | order = new BeerOrder { 209 | brand: 'Budweiser' 210 | quantity: 20 211 | address: new Address( … ) 212 | } 213 | 214 | Meteor.call BeerOrder, order 215 | 216 | class BeerOrderApi extends Space.messaging.Api 217 | @method BeerOrder, (order) -> # No need to check! 218 | ``` 219 | 220 | ## 1.0.0 221 | #### Breaking Changes: 222 | - Upgrades to `space:base@2.0.0` which had some (minor) breaking changes to the modules and application API. 223 | 224 | ## 0.5.0 225 | #### Breaking Changes: 226 | - Removed the usage of futures for method apis 227 | 228 | ## 0.4.0 229 | ### Breaking Changes: 230 | - Simplified the messaging api by keeping the event and command bus as simple 231 | as possible and introducing a special `Space.messaging.Api` class that makes 232 | working with async Meteor methods easier. 233 | - Removes the options hash for `@handle` and `@on` methods of 234 | `Space.messaging.Controller`. There are no options anymore because messages 235 | can't cross the client/server boundary anmore. Use `Space.messaging.Api` for 236 | that. 237 | - The api for `Space.messaging.Controller` has changed a bit, use `@handle` 238 | only for commands and `@on` for events. 239 | 240 | ### Improvements: 241 | - Api is much simpler now 242 | - You can send anything as command or event, it doesn't have to be a subclass 243 | 244 | ### New Features: 245 | `Space.messaging.Api` was introduces which is a convenient abstraction layer 246 | over Meteor methods to unify synchronouse and asynchronous method handling. It 247 | sets up a future for each method call and hands it over to the method handler 248 | together with the arguments and method context. This way you can work with 249 | Promises and other async stuff without having to deal with setting up futures. 250 | It also unifies the way you return stuff to the client, because you always 251 | use the future for that. 252 | 253 | ## 0.3.5 254 | Adds static `@method` function to `Space.messaging.Controller` that sets up 255 | a Meteor method with a future to reduce boilerplate for async methods. 256 | 257 | ## 0.3.4 258 | Adds `toPlainObject` method to `Space.messaging.Serializable` so that events 259 | and commands can easily be casted to DTOs instead of class instances. 260 | 261 | ## 0.3.3 262 | Allow (optionally) to provide source id of events as first parameter. This is 263 | more convenient in scenarios where data is handed over from other places. 264 | 265 | ## 0.3.2 266 | Adds short-hand API for handling controller messages 267 | 268 | ## 0.3.1 269 | Improves error handling for `Space.messaging.Controller` when handling events 270 | in the callback that is bound via `Meteor.bindEnvironment`. Now you can just 271 | throw `Meteor.Error` instances and they are correctly routed back to the client. 272 | 273 | ## 0.3.0 274 | Removes hooks for message handling because there is no real use case for it 275 | 276 | ## 0.2.1 277 | Fixes bug with binding message handlers to Meteor environment and controller 278 | instances. 279 | 280 | ## 0.2.0 281 | Simplified serializable and event API and made it more flexible 282 | 283 | ## 0.1.0 284 | Extracted messaging related code from space:cqrs into this package 285 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # space:messaging [![Circle CI](https://circleci.com/gh/meteor-space/messaging.svg?style=svg)](https://circleci.com/gh/meteor-space/messaging) 2 | 3 | **Rock solid Messaging Infrastructure for Space applications** 4 | 5 | This package provides infrastructure to build your Meteor applications 6 | around rock solid messaging principles that make your code strongly checked 7 | and thus reduces errors caused by API changes within the system. 8 | 9 | ## Installation 10 | `meteor add space:messaging` 11 | 12 | ## Documentation 13 | Please look through the tests to get a feeling what this package can do for you. 14 | I hope to find time to write some more documentation together soon ;-) 15 | 16 | ## Contributing 17 | In lieu of a formal styleguide, take care to maintain the existing coding style. 18 | Add unit / integration tests for any new or changed functionality. 19 | 20 | ## Run the tests 21 | `meteor test-packages ./` 22 | 23 | ## Release History 24 | You can find the release history in the [changelog](https://github.com/meteor-space/messaging/blob/master/CHANGELOG.md) 25 | 26 | ## License 27 | Copyright (c) 2015 Code Adventure 28 | Licensed under the MIT license. 29 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 0.10.33 4 | environment: 5 | PACKAGE_DIRS: packages 6 | pre: 7 | - curl https://install.meteor.com | /bin/sh 8 | dependencies: 9 | pre: 10 | - npm install -g mgp 11 | - npm install -g spacejam 12 | override: 13 | - mgp 14 | test: 15 | override: 16 | - spacejam test-packages ./ 17 | -------------------------------------------------------------------------------- /git-packages.json: -------------------------------------------------------------------------------- 1 | {} 2 | 3 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: 'Messaging infrastructure for Space applications.', 3 | name: 'space:messaging', 4 | version: '3.3.1', 5 | git: 'https://github.com/meteor-space/messaging.git' 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | 10 | api.versionsFrom('1.2.0.1'); 11 | 12 | api.use([ 13 | 'coffeescript', 14 | 'underscore', 15 | 'check', 16 | 'ejson', 17 | 'ecmascript', 18 | 'space:base@4.1.3' 19 | ]); 20 | 21 | // SHARED 22 | api.addFiles([ 23 | 'source/module.coffee', 24 | 'source/mixins/declarative-mappings.js', 25 | 'source/mixins/static-handlers.coffee', 26 | 'source/mixins/event-subscribing.js', 27 | 'source/mixins/event-publishing.coffee', 28 | 'source/mixins/command-sending.coffee', 29 | 'source/mixins/application-helpers.coffee', 30 | 'source/mixins/ejsonable.js', 31 | 'source/mixins/versionable.js' 32 | ]); 33 | 34 | // SERVER 35 | api.addFiles([ 36 | 'source/mixins/command-handling.js' 37 | ], 'server'); 38 | 39 | // SHARED 40 | api.addFiles([ 41 | 'source/helpers.js', 42 | 'source/value-objects/guid.coffee', 43 | 'source/serializables/event.js', 44 | 'source/serializables/command.js', 45 | 'source/serializables/error.js', 46 | 'source/event_bus.coffee', 47 | 'source/command_bus.coffee', 48 | 'source/controller.js', 49 | 'source/tracker.coffee', 50 | 'source/publication.coffee', 51 | 'source/api.coffee' 52 | ]); 53 | 54 | }); 55 | 56 | Package.onTest(function(api) { 57 | 58 | api.use([ 59 | 'coffeescript', 60 | 'underscore', 61 | 'check', 62 | 'ejson', 63 | 'mongo', 64 | 'ecmascript', 65 | 'space:testing@3.0.2', 66 | 'space:testing-messaging@3.0.1', 67 | 'space:messaging', 68 | 'practicalmeteor:munit@2.1.5' 69 | ]); 70 | 71 | api.addFiles([ 72 | 'tests/unit/serializable.unit.coffee', 73 | 'tests/unit/serializables/event.unit.coffee', 74 | 'tests/unit/serializables/command.unit.coffee', 75 | 'tests/unit/serializables/space-error.tests.js', 76 | 'tests/unit/event_bus.unit.coffee', 77 | 'tests/unit/command_bus.unit.coffee', 78 | 'tests/unit/api.unit.coffee', 79 | 'tests/unit/value-objects/guid.unit.coffee', 80 | 'tests/unit/helpers.tests.js', 81 | 'tests/unit/mixins/versionable.tests.js', 82 | 'tests/unit/mixins/event-subscribing.tests.js', 83 | 'tests/integration/controller_event_subscribing.js', 84 | 'tests/integration/test-app.js' 85 | ]); 86 | 87 | api.addFiles([ 88 | 'tests/integration/controller_command_handling.js', 89 | 'tests/integration/handling-api-messages.js', 90 | 'tests/unit/mixins/command-handling.tests.js' 91 | ], 'server'); 92 | 93 | }); 94 | -------------------------------------------------------------------------------- /source/api.coffee: -------------------------------------------------------------------------------- 1 | 2 | class Space.messaging.Api extends Space.Object 3 | 4 | @mixin [ 5 | Space.messaging.DeclarativeMappings 6 | Space.messaging.StaticHandlers 7 | Space.messaging.CommandSending 8 | Space.messaging.EventPublishing 9 | ] 10 | 11 | methods: -> [] 12 | 13 | # Register a handler for a Meteor method and add it as 14 | # method to instance to simplify testing of methods. 15 | @method: (type, handler) -> 16 | @_setupHandler type, handler 17 | @_registerMethod type, @_setupMethod(type) 18 | 19 | # Sugar for sending messages to the server 20 | @send: (message, callback) -> 21 | Meteor.call message.typeName(), message, callback 22 | 23 | # Register the method statically, so that is done only once 24 | @_registerMethod: (name, body) -> 25 | method = {} 26 | method[name] = body 27 | Meteor.methods method 28 | 29 | @_setupMethod: (type) -> 30 | name = type.toString() 31 | @_handlers ?= {} 32 | handlers = @_handlers 33 | return (param) -> 34 | try type = Space.resolvePath(name) 35 | if type.isSerializable then check param, type 36 | # Provide the method context to bound handler 37 | args = [this].concat Array::slice.call(arguments) 38 | handlers[name].bound.apply null, args 39 | 40 | onDependenciesReady: -> 41 | @_setupDeclarativeMappings 'methods', @_setupDeclarativeHandler 42 | @_bindHandlersToInstance() 43 | 44 | _setupDeclarativeHandler: (handler, type) => 45 | existingHandler = @_getHandlerFor type 46 | if existingHandler? 47 | @constructor._setupHandler type, handler 48 | else 49 | @constructor.method type, handler 50 | -------------------------------------------------------------------------------- /source/command_bus.coffee: -------------------------------------------------------------------------------- 1 | 2 | class Space.messaging.CommandBus extends Space.Object 3 | 4 | dependencies: { 5 | meteor: 'Meteor' 6 | api: 'Space.messaging.Api' 7 | } 8 | 9 | _handlers: null 10 | _onSendCallbacks: null 11 | 12 | constructor: -> 13 | super 14 | @_handlers = {} 15 | @_onSendCallbacks = [] 16 | 17 | send: (command, callback) -> 18 | if @meteor.isServer 19 | # ON THE SERVER 20 | handler = @_handlers[command.typeName()] 21 | cb(command) for cb in @_onSendCallbacks 22 | if !handler? 23 | message = "Missing command handler for <#{command.typeName()}>." 24 | throw new Error message 25 | handler(command, callback) 26 | else 27 | # ON THE CLIENT 28 | @api.send(command, callback) 29 | 30 | registerHandler: (typeName, handler, overrideExisting) -> 31 | if @_handlers[typeName]? and !overrideExisting 32 | throw new Error "There is already an handler for #{typeName} commands." 33 | @_handlers[typeName] = handler 34 | 35 | overrideHandler: (typeName, handler) -> 36 | @registerHandler typeName, handler, true 37 | 38 | getHandlerFor: (commandType) -> @_handlers[commandType] 39 | 40 | getHandledCommandTypes: -> commandType for commandType of @_handlers 41 | 42 | hasHandlerFor: (commandType) -> @getHandlerFor(commandType)? 43 | 44 | onSend: (handler) -> @_onSendCallbacks.push handler 45 | -------------------------------------------------------------------------------- /source/controller.js: -------------------------------------------------------------------------------- 1 | 2 | Space.Object.extend('Space.messaging.Controller', { 3 | mixin: [ 4 | Space.messaging.EventSubscribing, 5 | Space.messaging.EventPublishing, 6 | Space.messaging.CommandSending 7 | ] 8 | }); 9 | 10 | if (Meteor.isServer) { 11 | Space.messaging.Controller.mixin(Space.messaging.CommandHandling); 12 | } 13 | -------------------------------------------------------------------------------- /source/event_bus.coffee: -------------------------------------------------------------------------------- 1 | 2 | class Space.messaging.EventBus extends Space.Object 3 | 4 | _eventHandlers: null 5 | _onPublishCallbacks: null 6 | 7 | constructor: -> 8 | @_eventHandlers = {} 9 | @_onPublishCallbacks = [] 10 | 11 | publish: (event) -> 12 | eventType = event.typeName() 13 | callback(event) for callback in @_onPublishCallbacks 14 | if not @_eventHandlers[eventType]? then return 15 | handler(event) for handler in @_eventHandlers[eventType] 16 | 17 | onPublish: (handler) -> @_onPublishCallbacks.push handler 18 | 19 | subscribeTo: (typeName, handler) -> (@_eventHandlers[typeName] ?= []).push handler 20 | 21 | getHandledEventTypes: -> eventType for eventType of @_eventHandlers 22 | 23 | hasHandlerFor: (eventType) -> @_eventHandlers[eventType]? 24 | -------------------------------------------------------------------------------- /source/helpers.js: -------------------------------------------------------------------------------- 1 | Space.messaging.define = (BaseType, ...options) => { 2 | 3 | if (!BaseType.isSerializable) { 4 | throw new Error('BaseType must extend Space.messaging.Serializable'); 5 | } 6 | 7 | let namespace = null; 8 | let definitions = null; 9 | let subTypes = {}; 10 | 11 | switch (options.length) { 12 | case 0: 13 | throw new Error(`Space.messaging.define is missing options for defining sub typs of ${BaseType}.`); 14 | case 1: 15 | if (_.isObject(options[0])) { 16 | // VALID: Definitions but no namespace provided 17 | namespace = ''; 18 | definitions = options[0]; 19 | } else { 20 | throw new Error(`Space.messaging.define is missing definitions for ${BaseType}.`); 21 | } 22 | break; 23 | default: 24 | // VALID: Namespace and definitions provided 25 | namespace = options[0].toString(); 26 | definitions = options[1]; 27 | break; 28 | } 29 | 30 | _.each(definitions, (fields, className) => { 31 | let classPath = className; 32 | if (namespace !== '') classPath = `${namespace}.${className}`; 33 | let SubType = BaseType.extend(classPath); 34 | SubType.prototype.fields = () => _.extend(BaseType.prototype.fields(), fields); 35 | Space.resolvePath(namespace)[className] = SubType; 36 | subTypes[classPath] = SubType; 37 | }); 38 | 39 | return subTypes; 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /source/mixins/application-helpers.coffee: -------------------------------------------------------------------------------- 1 | 2 | # Add basic messaging capabilities to Space applications 3 | Space.Application.mixin { 4 | 5 | dependencies: { 6 | eventBus: 'Space.messaging.EventBus' 7 | commandBus: 'Space.messaging.CommandBus' 8 | } 9 | 10 | publish: -> @eventBus.publish.apply(@eventBus, arguments) 11 | 12 | subscribeTo: (type, handler) -> @eventBus.subscribeTo.apply(@eventBus, arguments) 13 | 14 | send: (command) -> @commandBus.send.apply(@commandBus, arguments) 15 | 16 | } 17 | -------------------------------------------------------------------------------- /source/mixins/command-handling.js: -------------------------------------------------------------------------------- 1 | Space.messaging.CommandHandling = { 2 | 3 | dependencies: { 4 | commandBus: 'Space.messaging.CommandBus' 5 | }, 6 | 7 | ERRORS: { 8 | noCommandHandlersDefined() { 9 | return 'Please define a ::commandHandlers method that returns array of mappings.'; 10 | }, 11 | invalidCommandType(commandType) { 12 | return `Cannot register command handler for ${commandType}`; 13 | }, 14 | invalidCommandHandler() { 15 | return "You have to provide a command handler function."; 16 | }, 17 | noCommandHandlerFound(typeName) { 18 | return `No command handler found for <${typeName}>`; 19 | } 20 | }, 21 | 22 | _commandHandlers: null, 23 | 24 | onConstruction() { 25 | if (this._commandHandlers === null) { 26 | this._commandHandlers = {}; 27 | } 28 | }, 29 | 30 | onDependenciesReady() { 31 | this._setupCommandHandling(); 32 | }, 33 | 34 | canHandleCommand(command) { 35 | return this._getCommandHandlerFor(command) !== undefined; 36 | }, 37 | 38 | register(commandType, handler) { 39 | if (!commandType) { 40 | throw new Error(this.ERRORS.invalidCommandType(commandType)); 41 | } else if (!handler) { 42 | throw new Error(this.ERRORS.invalidCommandHandler()); 43 | } 44 | this._commandHandlers[commandType.toString()] = handler; 45 | this.commandBus.registerHandler(commandType, this.underscore.bind(handler, this)); 46 | }, 47 | 48 | handle(command) { 49 | let handler = this._getCommandHandlerFor(command); 50 | if (!handler) { 51 | throw new Error(this.ERRORS.noCommandHandlerFound(command.typeName())); 52 | } 53 | handler.call(this, command); 54 | }, 55 | 56 | _setupCommandHandling() { 57 | if (!this.underscore.isFunction(this.commandHandlers)) return; 58 | this._setupDeclarativeMappings('commandHandlers', (handler, commandType) => { 59 | this.register(commandType, handler); 60 | }); 61 | }, 62 | 63 | _getCommandHandlerFor(command) { 64 | return this._commandHandlers[command.typeName()]; 65 | } 66 | }; 67 | 68 | _.deepExtend(Space.messaging.CommandHandling, Space.messaging.DeclarativeMappings); 69 | -------------------------------------------------------------------------------- /source/mixins/command-sending.coffee: -------------------------------------------------------------------------------- 1 | Space.messaging.CommandSending = { 2 | 3 | dependencies: { 4 | commandBus: 'Space.messaging.CommandBus' 5 | } 6 | 7 | send: -> @commandBus.send.apply(@commandBus, arguments) 8 | 9 | } 10 | -------------------------------------------------------------------------------- /source/mixins/declarative-mappings.js: -------------------------------------------------------------------------------- 1 | Space.messaging.DeclarativeMappings = { 2 | 3 | dependencies: { 4 | underscore: 'underscore' 5 | }, 6 | 7 | _setupDeclarativeMappings(map, setup) { 8 | let mappings = {}; 9 | let declarations = this[map](); 10 | if (!this.underscore.isArray(declarations)) { 11 | declarations = [declarations]; 12 | } 13 | declarations.unshift(mappings); 14 | this.underscore.extend.apply(null, declarations); 15 | this.underscore.each(mappings, setup); 16 | } 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /source/mixins/ejsonable.js: -------------------------------------------------------------------------------- 1 | 2 | // ========= HELPERS ========== // 3 | 4 | let generateTypeNameMethod = function(typeName) { 5 | return function() { 6 | return typeName; 7 | }; 8 | }; 9 | 10 | let fromJSONValueFunction = function(Class, json) { 11 | // Parse all fields that are set in the given json 12 | for (let field in Class.prototype.fields()) { 13 | if (json[field]) { 14 | json[field] = EJSON.fromJSONValue(json[field]); 15 | } 16 | } 17 | return new Class(json); 18 | }; 19 | 20 | Space.messaging.Ejsonable = { 21 | 22 | statics: { 23 | 24 | // Mark this class as serializable 25 | isSerializable: true, 26 | 27 | // Add unique type for serialization 28 | type(name) { 29 | Space.Object.type.call(this, name); 30 | this.prototype.typeName = this.toString = generateTypeNameMethod(name); 31 | EJSON.addType(name, _.partial(fromJSONValueFunction, this)); 32 | return this; 33 | }, 34 | 35 | fromData(raw) { 36 | let data = {}; 37 | _.each(this.prototype.fields(), function(Type, key) { 38 | if (raw[key] === undefined) return; 39 | let value = raw[key]; 40 | if (value._type !== undefined) { 41 | // This is a sub-serializable 42 | data[key] = Space.resolvePath(value._type).fromData(raw[key]); 43 | } else if (_.isArray(value)) { 44 | // This is an array of values / sub-serializables 45 | data[key] = value.map(function(v) { 46 | if (v._type !== undefined) { 47 | return Space.resolvePath(v._type).fromData(v); 48 | } else { 49 | return v; 50 | } 51 | }); 52 | } else { 53 | data[key] = value; 54 | } 55 | }); 56 | return new this(data); 57 | } 58 | 59 | }, 60 | 61 | // Mark this object as serializable 62 | isSerializable: true, 63 | 64 | /** 65 | * Recursivly turn this object and all it's sub-serializables into one 66 | * nested EJSON structure. Required by Meteor's EJSON package 67 | */ 68 | toJSONValue() { 69 | let fields = this.fields(); 70 | if (!fields || _.isEmpty(fields)) { 71 | // No special fields, simply parse instance to create deep copy 72 | return JSON.parse(JSON.stringify(this)); 73 | } else { 74 | // Fields defined, parse them through EJSON to support nested types 75 | let serialized = {}; 76 | for (let key in fields) { 77 | if (fields.hasOwnProperty(key) && this[key] !== undefined) { 78 | serialized[key] = EJSON.toJSONValue(this[key]); 79 | } 80 | } 81 | return serialized; 82 | } 83 | }, 84 | 85 | toData() { 86 | let data = { _type: this.typeName() }; 87 | _.each(this.fields(), (Type, key) => { 88 | if (this[key] === undefined) return; 89 | let value = this[key]; 90 | if (value.isSerializable) { 91 | // This is another serializable 92 | data[key] = value.toData(); 93 | } else if (_.isArray(value)) { 94 | // This is an array of sub values / Serializable 95 | data[key] = value.map(function(v) { 96 | return v.isSerializable ? v.toData() : v; 97 | }); 98 | } else { 99 | data[key] = value; 100 | } 101 | }); 102 | return data; 103 | } 104 | 105 | }; 106 | 107 | // Todo: Refactor to mixin only! This is just there to support current systems 108 | Space.Struct.extend(Space.messaging, 'Serializable', { 109 | mixin: [Space.messaging.Ejsonable] 110 | }); 111 | -------------------------------------------------------------------------------- /source/mixins/event-publishing.coffee: -------------------------------------------------------------------------------- 1 | Space.messaging.EventPublishing = { 2 | 3 | dependencies: { 4 | eventBus: 'Space.messaging.EventBus' 5 | } 6 | 7 | publish: (event) -> @eventBus.publish event 8 | 9 | } 10 | -------------------------------------------------------------------------------- /source/mixins/event-subscribing.js: -------------------------------------------------------------------------------- 1 | Space.messaging.EventSubscribing = { 2 | 3 | dependencies: { 4 | eventBus: 'Space.messaging.EventBus', 5 | meteor: 'Meteor' 6 | }, 7 | 8 | _eventHandlers: null, 9 | 10 | onConstruction() { 11 | if (this._eventHandlers === null) { 12 | this._eventHandlers = {}; 13 | } 14 | }, 15 | 16 | onDependenciesReady() { 17 | this._setupEventSubscribing(); 18 | }, 19 | 20 | canHandleEvent(event) { 21 | return this._getEventHandlerFor(event) !== undefined; 22 | }, 23 | 24 | subscribe(eventType, handler) { 25 | if (!eventType) { 26 | throw new Error(`Cannot register event handler for ${eventType}`); 27 | } else if (!handler) { 28 | throw new Error("You have to provide a handler function."); 29 | } 30 | this._eventHandlers[eventType.toString()] = handler; 31 | this.eventBus.subscribeTo(eventType, this._bindEventHandler(handler)); 32 | }, 33 | 34 | on(event) { 35 | let handler = this._getEventHandlerFor(event); 36 | if (!handler) { 37 | throw new Error(`No event handler found for <${event.typeName()}>`); 38 | } 39 | handler.call(this, event); 40 | }, 41 | 42 | _setupEventSubscribing() { 43 | if (!this.eventSubscriptions) return; 44 | this._setupDeclarativeMappings('eventSubscriptions', (handler, eventType) => { 45 | this.subscribe(eventType, handler); 46 | }); 47 | }, 48 | 49 | /** 50 | * All event handlers are bound to the meteor environment by default 51 | * so that the application code can mainly stay clear of having to 52 | * deal with these things. 53 | */ 54 | _bindEventHandler(handler) { 55 | if (this.meteor.isServer) { 56 | return this.meteor.bindEnvironment(handler, this._onException, this); 57 | } else { 58 | return this.underscore.bind(handler, this); 59 | } 60 | }, 61 | 62 | _getEventHandlerFor(event) { 63 | return this._eventHandlers[event.typeName()]; 64 | }, 65 | 66 | _onException(error) { throw error; } 67 | 68 | }; 69 | 70 | _.deepExtend(Space.messaging.EventSubscribing, Space.messaging.DeclarativeMappings); 71 | -------------------------------------------------------------------------------- /source/mixins/static-handlers.coffee: -------------------------------------------------------------------------------- 1 | Space.messaging.StaticHandlers = { 2 | 3 | dependencies: { 4 | underscore: 'underscore' 5 | } 6 | 7 | statics: { 8 | _handlers: null 9 | _setupHandler: (name, handler) -> 10 | @_ensureHandlersMap() 11 | @_handlers[name] = original: handler, bound: null 12 | _ensureHandlersMap: -> @_handlers ?= {} 13 | } 14 | 15 | _getHandlerFor: (method) -> 16 | @constructor._ensureHandlersMap() 17 | @constructor._handlers[method] 18 | 19 | _bindHandlersToInstance: -> 20 | handlers = @constructor._handlers 21 | for name, handler of handlers 22 | boundHandler = @underscore.bind handler.original, this 23 | handlers[name].bound = boundHandler 24 | 25 | } 26 | -------------------------------------------------------------------------------- /source/mixins/versionable.js: -------------------------------------------------------------------------------- 1 | Space.messaging.Versionable = { 2 | 3 | ERRORS: { 4 | dataTransformMethodMissing(version) { 5 | return `Missing method in Versionable class.`; 6 | } 7 | }, 8 | 9 | schemaVersion: 1, 10 | 11 | onConstruction(data) { 12 | if (_.isObject(data) && data.schemaVersion < this.schemaVersion) { 13 | this._transformLegacySchema(data); 14 | } 15 | }, 16 | 17 | _transformLegacySchema(data) { 18 | for (let version = data.schemaVersion; version < this.schemaVersion; version++) { 19 | let transformMethod = this[`transformFromVersion${version}`]; 20 | if (transformMethod === undefined) { 21 | throw new Error(this.ERRORS.dataTransformMethodMissing(version)); 22 | } else { 23 | transformMethod.call(this, data); 24 | } 25 | } 26 | data.schemaVersion = this.schemaVersion; 27 | } 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /source/module.coffee: -------------------------------------------------------------------------------- 1 | 2 | class Space.messaging extends Space.Module 3 | 4 | @publish this, 'Space.messaging' 5 | 6 | onInitialize: -> 7 | @injector.map('Space.messaging.EventBus').asSingleton() 8 | @injector.map('Space.messaging.CommandBus').asSingleton() 9 | @injector.map('Space.messaging.Api').asStaticValue() 10 | -------------------------------------------------------------------------------- /source/publication.coffee: -------------------------------------------------------------------------------- 1 | class Space.messaging.Publication extends Space.Object 2 | 3 | dependencies: { 4 | meteor: 'Meteor' 5 | } 6 | 7 | @mixin [ 8 | Space.messaging.DeclarativeMappings 9 | Space.messaging.StaticHandlers 10 | ] 11 | 12 | publications: -> [] 13 | 14 | @publish: (name, handler) -> 15 | @_setupHandler name, handler 16 | handlers = @_handlers 17 | @_registerPublication name, -> 18 | # Provide the publish context to bound handler as first argument 19 | args = [this].concat Array::slice.call(arguments) 20 | handlers[name].bound.apply null, args 21 | return this 22 | 23 | @_registerPublication: (name, callback) -> Meteor.publish name, callback 24 | 25 | onDependenciesReady: -> 26 | super 27 | @_setupDeclarativeMappings 'publications', @_setupDeclarativeHandler 28 | @_bindHandlersToInstance() 29 | 30 | _setupDeclarativeHandler: (handler, type) => 31 | existingHandler = @_getHandlerFor type 32 | if existingHandler? 33 | @constructor._setupHandler type, handler 34 | else 35 | @constructor.publish type, handler 36 | -------------------------------------------------------------------------------- /source/serializables/command.js: -------------------------------------------------------------------------------- 1 | 2 | Space.Struct.extend('Space.messaging.Command', { 3 | 4 | mixin: [ 5 | Space.messaging.Ejsonable, 6 | Space.messaging.Versionable 7 | ], 8 | 9 | Constructor(params) { 10 | let data = params || {}; 11 | return Space.Struct.call(this, data); 12 | }, 13 | 14 | fields() { 15 | let fields = Space.Struct.prototype.fields.call(this); 16 | // Add default fields to all commands 17 | fields.schemaVersion = Match.Optional(Match.Integer); 18 | return fields; 19 | } 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /source/serializables/error.js: -------------------------------------------------------------------------------- 1 | // Make errors serializable 2 | Space.Error.mixin(Space.messaging.Ejsonable); 3 | Space.Error.type('Space.Error'); 4 | 5 | // Make errors versionable 6 | Space.Error.mixin(Space.messaging.Versionable); 7 | Space.Error.prototype.fields = _.wrap(Space.Error.prototype.fields, 8 | function(original) { 9 | const fields = original.call(this); 10 | fields.schemaVersion = Match.Optional(Match.Integer); 11 | return fields; 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /source/serializables/event.js: -------------------------------------------------------------------------------- 1 | 2 | Space.Struct.extend('Space.messaging.Event', { 3 | 4 | mixin: [ 5 | Space.messaging.Ejsonable, 6 | Space.messaging.Versionable 7 | ], 8 | 9 | Constructor(params) { 10 | let data = params || {}; 11 | return Space.Struct.call(this, data); 12 | }, 13 | 14 | fields() { 15 | let fields = Space.Struct.prototype.fields.call(this); 16 | // Add default fields to all events 17 | fields.schemaVersion = Match.Optional(Match.Integer); 18 | return fields; 19 | } 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /source/tracker.coffee: -------------------------------------------------------------------------------- 1 | class Space.messaging.Tracker extends Space.Object 2 | 3 | dependencies: { 4 | tracker: 'Tracker' 5 | meteor: 'Meteor' 6 | } 7 | 8 | onDependenciesReady: -> @tracker.autorun => @autorun() 9 | 10 | autorun: -> 11 | -------------------------------------------------------------------------------- /source/value-objects/guid.coffee: -------------------------------------------------------------------------------- 1 | 2 | class @Guid extends Space.Struct 3 | 4 | @mixin [Space.messaging.Ejsonable] 5 | 6 | @type 'Guid' 7 | @fields: id: String 8 | 9 | # ============== STATIC ============= # 10 | 11 | # Checks valid 128-bit UUIDs version 4 12 | @REGEXP = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i 13 | 14 | # Generates 128-bit UUIDs version 4 15 | # http://en.wikipedia.org/wiki/Universally_unique_identifier 16 | @generate: -> 17 | 18 | time = new Date().getTime() 19 | 20 | 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace /[xy]/g, (current) -> 21 | 22 | random = (time + Math.random()*16) % 16 | 0 23 | yValue = '89ab'.charAt(Math.floor(Math.random()*3.99)) 24 | time = Math.floor time / 16 25 | char = if current == 'x' then random else yValue 26 | 27 | return char.toString 16 28 | 29 | @isValid: (guid) -> if guid? then @REGEXP.test(guid.toString()) else false 30 | 31 | # ============== PROTOTYPE ============= # 32 | 33 | # Param can be a string or an object { id: String } 34 | constructor: (data) -> 35 | id = null 36 | if data? 37 | if data.id then id = data.id else id = data 38 | throw new Error "Invalid guid given: #{id}" unless Guid.isValid(id) 39 | else 40 | id = Guid.generate() 41 | 42 | @id = id.toString() # convert to string representation 43 | Object.freeze this 44 | 45 | valueOf: -> @id 46 | toString: -> @id 47 | toJSON: -> @id 48 | toJSONValue: -> @id 49 | 50 | equals: (guid) -> (guid instanceof Guid) and guid.valueOf() == @valueOf() 51 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export METEOR_PACKAGE_DIRS='packages' 4 | 5 | if [ "$PORT" ]; then 6 | meteor test-packages ./ --port $PORT 7 | else 8 | meteor test-packages ./ 9 | fi 10 | -------------------------------------------------------------------------------- /tests/integration/controller_command_handling.js: -------------------------------------------------------------------------------- 1 | 2 | describe("Handling commands", function() { 3 | 4 | beforeEach(function() { 5 | this.myCommand = new MyCommand({ 6 | value: new MyValue({name: 'test'}) 7 | }); 8 | this.anotherCommand = new AnotherCommand({ 9 | myCustomTarget: '123' 10 | }); 11 | }); 12 | 13 | it("handles commands", function() { 14 | let myHandler = sinon.spy(); 15 | let anotherHandler = sinon.spy(); 16 | 17 | // Define a controller, declare handlers 18 | MyController = Space.messaging.Controller.extend('MyController', { 19 | commandHandlers: function() { 20 | return { 21 | 'MyCommand': myHandler, 22 | 'AnotherCommand': anotherHandler 23 | }; 24 | } 25 | }); 26 | 27 | // Integrate the controller in our test app 28 | let ControllerTestApp = Space.Application.define('ControllerTestApp', { 29 | requiredModules: ['Space.messaging'], 30 | singletons: ['MyController'] 31 | }); 32 | 33 | // Startup app and send the commands through the bus 34 | let app = new ControllerTestApp(); 35 | let myController = app.injector.get('MyController'); 36 | let myCallback = () => {}; 37 | app.start(); 38 | app.commandBus.send(this.myCommand); 39 | app.commandBus.send(this.anotherCommand, myCallback); 40 | 41 | // Expect that the controller handled the commands 42 | expect(myHandler).to.have.been.calledWith(this.myCommand).calledOn(myController); 43 | expect(anotherHandler).to.have.been.calledWithExactly(this.anotherCommand, myCallback).calledOn(myController); 44 | 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/integration/controller_event_subscribing.js: -------------------------------------------------------------------------------- 1 | 2 | describe("Event subscribing", function() { 3 | 4 | beforeEach(function() { 5 | this.myEvent = new MyEvent({ 6 | value: new MyValue({ name: 'test' }) 7 | }); 8 | this.anotherEvent = new AnotherEvent({ }); 9 | }); 10 | 11 | it("subscribes to events", function() { 12 | 13 | let mySubscription = sinon.spy(); 14 | let anotherSubscription = sinon.spy(); 15 | 16 | // Define a controller that uses the `events` API to declare handlers 17 | MyController = Space.messaging.Controller.extend('MyController', { 18 | eventSubscriptions: function() { 19 | return [{ 20 | 'MyEvent': mySubscription, 21 | 'AnotherEvent': anotherSubscription 22 | }]; 23 | } 24 | }); 25 | 26 | // Integrate the controller in our test app 27 | let ControllerTestApp = Space.Application.define('ControllerTestApp', { 28 | requiredModules: ['Space.messaging'], 29 | afterInitialize: function() { 30 | this.injector.map('MyController').toSingleton(MyController); 31 | } 32 | }); 33 | 34 | // Startup app and publish event through the bus 35 | let app = new ControllerTestApp(); 36 | let myController = app.injector.get('MyController'); 37 | app.start(); 38 | app.eventBus.publish(this.myEvent); 39 | app.eventBus.publish(this.anotherEvent); 40 | 41 | // Expect that the controller subscribed to the events 42 | expect(mySubscription).to.have.been.calledWithExactly(this.myEvent).calledOn(myController); 43 | expect(anotherSubscription).to.have.been.calledWithExactly(this.anotherEvent).calledOn(myController); 44 | 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/integration/handling-api-messages.js: -------------------------------------------------------------------------------- 1 | describe("handling api messages", function() { 2 | 3 | beforeEach(function() { 4 | this.command = new MyCommand({ 5 | value: new MyValue({ name: 'good-value' }) 6 | }); 7 | }); 8 | 9 | it("sends a handled command on the server-side command bus if passes validation", function() { 10 | MyApp.test(MyApi).send(this.command).expect([this.command]); 11 | }); 12 | 13 | it("does not send a handled command on the server-side command bus if fails validation", function() { 14 | this.command.value = new MyValue({ name: 'bad-value' }); 15 | MyApp.test(MyApi).send(this.command).expect([]); 16 | }); 17 | 18 | it("receives any values that are compatible with meteor methods", function() { 19 | let id = '123'; 20 | MyApp.test(MyApi).call('UncheckedMethod', id).expect([ 21 | new AnotherCommand({ myCustomTarget: id }) 22 | ]); 23 | }); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /tests/integration/test-app.js: -------------------------------------------------------------------------------- 1 | this.MyValue = Space.messaging.Serializable.extend('MyValue', { 2 | fields() { 3 | return { name: String }; 4 | } 5 | }); 6 | 7 | Space.messaging.define(Space.messaging.Event, { 8 | MyEvent: { value: MyValue }, 9 | AnotherEvent: {} 10 | }); 11 | 12 | Space.messaging.define(Space.messaging.Command, { 13 | MyCommand: { value: MyValue }, 14 | AnotherCommand: { myCustomTarget: String } 15 | }); 16 | 17 | if (Meteor.isServer) { 18 | 19 | this.MyApi = Space.messaging.Api.extend('MyApi', { 20 | methods() { 21 | return { 22 | // Simulate some simple method validation 23 | 'MyCommand'(_, command) { 24 | if (command.value.name === 'good-value') { 25 | this.send(command); 26 | } 27 | }, 28 | // Showcase that you can also call your methods "like normal" 29 | 'UncheckedMethod'(_, id) { 30 | this.send(new AnotherCommand({ 31 | myCustomTarget: id 32 | })); 33 | } 34 | }; 35 | } 36 | }); 37 | 38 | this.MyCommandHandler = Space.Object.extend({ 39 | mixin: [ 40 | Space.messaging.CommandHandling 41 | ], 42 | commandHandlers() { 43 | return [{ 44 | 'MyCommand'() {}, 45 | 'AnotherCommand'() {} 46 | }]; 47 | } 48 | }); 49 | 50 | this.MyApp = Space.Application.extend({ 51 | requiredModules: ['Space.messaging'], 52 | singletons: ['MyApi', 'MyCommandHandler'] 53 | }); 54 | 55 | } 56 | 57 | -------------------------------------------------------------------------------- /tests/unit/api.unit.coffee: -------------------------------------------------------------------------------- 1 | describe 'Space.messaging - Api', -> 2 | 3 | handler = sinon.stub() 4 | methodName = 'ApiTest.TestMethod' 5 | 6 | ApiTest = Space.namespace 'ApiTest' 7 | 8 | class ApiTest.TestType extends Space.messaging.Serializable 9 | @type 'ApiTest.TestType' 10 | 11 | class ApiTest.MyApi extends Space.messaging.Api 12 | @method methodName, handler 13 | methods: -> ['ApiTest.TestType': handler] 14 | 15 | beforeEach -> 16 | @api = new ApiTest.MyApi underscore: _ 17 | @api.onDependenciesReady() 18 | 19 | describe 'registering methods', -> 20 | 21 | it.server 'statically registers a meteor method', -> 22 | expect(Meteor.default_server.method_handlers[methodName]).to.exist 23 | 24 | it 'provides the correct arguments to the method handler', -> 25 | arg1 = {} 26 | arg2 = {} 27 | Meteor.call methodName, arg1, arg2, -> 28 | expect(handler).to.have.been.calledWithMatch sinon.match.object, arg1, arg2 29 | 30 | it.server 'registers a method with the type name', -> 31 | expect(Meteor.default_server.method_handlers[ApiTest.TestType.toString()]).to.exist 32 | 33 | it 'checks the param for typed methods', -> 34 | param = new ApiTest.TestType() 35 | Meteor.call ApiTest.TestType.toString(), param, -> 36 | expect(handler).to.have.been.calledWithMatch( 37 | sinon.match.object, sinon.match.instanceOf(ApiTest.TestType) 38 | ) 39 | 40 | it.server 'throws exception if the check fails', -> 41 | expect(-> Meteor.call ApiTest.TestType.toString(), null).to.throw Error 42 | 43 | describe.server 'sending messages to the server', -> 44 | 45 | it 'provides a static sugar to Meteor.call', -> 46 | message = new ApiTest.TestType() 47 | Space.messaging.Api.send message 48 | expect(handler).to.have.been.calledWith sinon.match(message) 49 | -------------------------------------------------------------------------------- /tests/unit/command_bus.unit.coffee: -------------------------------------------------------------------------------- 1 | 2 | CommandBus = Space.messaging.CommandBus 3 | 4 | describe 'Space.messaging.CommandBus', -> 5 | 6 | class TestCommand extends Space.messaging.Command 7 | @type 'Space.messaging.CommandBusStubCommand' 8 | 9 | beforeEach -> 10 | @api = send: sinon.spy() 11 | @commandBus = new CommandBus { meteor: Meteor, api: @api } 12 | @testCommand = new TestCommand 13 | @handler = sinon.spy() 14 | 15 | it 'extends space object to be js compatible', -> 16 | expect(CommandBus).to.extend Space.Object 17 | 18 | describe 'registering handlers', -> 19 | 20 | it 'protects against multiple handler registrations for any one command', -> 21 | first = sinon.spy() 22 | second = sinon.spy() 23 | @commandBus.registerHandler TestCommand, first 24 | registerTwice = => @commandBus.registerHandler TestCommand, second 25 | expect(registerTwice).to.throw Error 26 | 27 | it 'can provide the types of commands it can handle', -> 28 | handler = -> 29 | @commandBus.registerHandler TestCommand, handler 30 | expect(@commandBus.getHandledCommandTypes()).to.deep.equal( 31 | ['Space.messaging.CommandBusStubCommand'] 32 | ) 33 | 34 | it 'allows handler registrations to be overridden', -> 35 | first = sinon.spy() 36 | second = sinon.spy() 37 | @commandBus.registerHandler TestCommand, first 38 | @commandBus.registerHandler TestCommand, second, true 39 | expect(@commandBus.getHandlerFor TestCommand).to.equal second 40 | 41 | describe 'sending commands', -> 42 | 43 | it.server 'calls the registered handler with the command', -> 44 | @commandBus.registerHandler TestCommand, @handler 45 | @commandBus.send @testCommand 46 | expect(@handler).to.have.been.calledWith @testCommand 47 | 48 | it.server 'calls the registered handler with an optional callback', -> 49 | @commandBus.registerHandler TestCommand, @handler 50 | callback = -> 51 | @commandBus.send @testCommand, callback 52 | expect(@handler).to.have.been.calledWithExactly @testCommand, callback 53 | 54 | it.client 'uses api.send for sending commands with optional callback', -> 55 | @commandBus.send @testCommand 56 | expect(@api.send).to.have.been.calledWith @testCommand 57 | 58 | it.client 'uses api.send for sending commands', -> 59 | callback = -> 60 | @commandBus.send @testCommand, callback 61 | expect(@api.send).to.have.been.calledWithExactly @testCommand, callback 62 | 63 | describe 'onSend callbacks', -> 64 | 65 | it.server 'calls all callbacks when sending a command', -> 66 | firstCallback = sinon.spy() 67 | secondCallback = sinon.spy() 68 | @commandBus.onSend firstCallback 69 | @commandBus.onSend secondCallback 70 | @commandBus.registerHandler TestCommand, @handler 71 | @commandBus.send @testCommand 72 | expect(firstCallback).to.have.been.calledWithExactly @testCommand 73 | expect(secondCallback).to.have.been.calledWithExactly @testCommand 74 | -------------------------------------------------------------------------------- /tests/unit/event_bus.unit.coffee: -------------------------------------------------------------------------------- 1 | 2 | EventBus = Space.messaging.EventBus 3 | 4 | describe 'Space.messaging.EventBus', -> 5 | 6 | class TestEvent extends Space.messaging.Event 7 | @type 'Space.messaging.__tests__.EventBusStubEvent' 8 | 9 | beforeEach -> 10 | @eventBus = new EventBus() 11 | @testEvent = new TestEvent() 12 | @handler = sinon.spy() 13 | 14 | it 'extends space object to be js compatible', -> 15 | expect(EventBus).to.extend Space.Object 16 | 17 | describe 'subscribing to an event', -> 18 | 19 | it 'allows multiple subscriptions to one event', -> 20 | first = sinon.spy() 21 | second = sinon.spy() 22 | @eventBus.subscribeTo TestEvent, first 23 | @eventBus.subscribeTo TestEvent, second 24 | @eventBus.publish @testEvent 25 | 26 | expect(first).to.have.been.calledWith @testEvent 27 | expect(second).to.have.been.calledWith @testEvent 28 | 29 | it 'can provide the types of events it can handle', -> 30 | subscriber = -> 31 | @eventBus.subscribeTo TestEvent, subscriber 32 | expect(@eventBus.getHandledEventTypes()).to.deep.equal( 33 | ['Space.messaging.__tests__.EventBusStubEvent'] 34 | ) 35 | 36 | describe 'publishing events', -> 37 | 38 | it 'calls the subscription with the event', -> 39 | @eventBus.subscribeTo TestEvent, @handler 40 | @eventBus.publish @testEvent 41 | expect(@handler).to.have.been.calledWithExactly @testEvent 42 | 43 | describe 'onPublish callbacks', -> 44 | 45 | it.server 'calls all callbacks when publishing an event', -> 46 | firstCallback = sinon.spy() 47 | secondCallback = sinon.spy() 48 | @eventBus.onPublish firstCallback 49 | @eventBus.onPublish secondCallback 50 | @eventBus.publish @testEvent 51 | expect(firstCallback).to.have.been.calledWithExactly @testEvent 52 | expect(secondCallback).to.have.been.calledWithExactly @testEvent 53 | -------------------------------------------------------------------------------- /tests/unit/helpers.tests.js: -------------------------------------------------------------------------------- 1 | describe("Space.messaging.define", function() { 2 | 3 | describe("batch defining serializable objects", function() { 4 | 5 | it("returns object with defined serializables", function() { 6 | const definedSerializables = Space.messaging.define(Space.messaging.Event, { 7 | FirstEvent: {}, 8 | SecondEvent: {} 9 | }); 10 | expect(definedSerializables.FirstEvent).to.extend(Space.messaging.Event); 11 | expect(definedSerializables.SecondEvent).to.extend(Space.messaging.Event); 12 | }); 13 | 14 | }); 15 | 16 | describe("use with a Space.namespace", function() { 17 | 18 | beforeEach(function() { 19 | this.myNamespace = Space.namespace('My.define.Namespace'); 20 | }); 21 | 22 | it("creates namespaced serializables when passing a Space.namespace object as the second argument", function() { 23 | Space.messaging.define(Space.messaging.Event, this.myNamespace, { 24 | FirstEvent: {}, 25 | SecondEvent: {} 26 | }); 27 | expect(this.myNamespace.FirstEvent).to.extend(Space.messaging.Event); 28 | expect(this.myNamespace.SecondEvent).to.extend(Space.messaging.Event); 29 | }); 30 | 31 | it("creates namespaced serializables when passing the string reference of a Space.namespace as the second argument", function() { 32 | Space.messaging.define(Space.messaging.Event, 'My.define.Namespace', { 33 | ThirdEvent: {} 34 | }); 35 | expect(this.myNamespace.ThirdEvent).to.extend(Space.messaging.Event); 36 | }); 37 | }) 38 | 39 | }); 40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/unit/mixins/command-handling.tests.js: -------------------------------------------------------------------------------- 1 | 2 | describe("Space.messaging.CommandHandling", function() { 3 | 4 | const MyClass = Space.Object.extend({ 5 | mixin: Space.messaging.CommandHandling 6 | }); 7 | const MyCommand = Space.messaging.Command.extend( 8 | 'Space.messaging.CommandHandling.__Test__.MyCommand', 9 | {} 10 | ); 11 | 12 | it("does not provide empty default", function() { 13 | expect(MyClass.prototype.commandHandlers).not.to.exist; 14 | }); 15 | 16 | it("does not throw error if no command handlers have been defined", function() { 17 | const createWithoutHandlers = function() { 18 | new MyClass({ underscore: _ }).onDependenciesReady(); 19 | }; 20 | expect(createWithoutHandlers).not.to.throw(Error); 21 | }); 22 | 23 | describe("public methods", function() { 24 | 25 | beforeEach(function () { 26 | this.myClassInstance = new MyClass({ 27 | meteor: Meteor, 28 | commandBus: new Space.messaging.CommandBus, 29 | underscore: _ 30 | }); 31 | this.myCommandInstance = new MyCommand(); 32 | }); 33 | 34 | describe("canHandleCommand", function () { 35 | 36 | it("returns true if object has a registered handler function", function () { 37 | const handler = sinon.spy(); 38 | this.myClassInstance.register(MyCommand, handler); 39 | expect(this.myClassInstance.canHandleCommand(this.myCommandInstance)).to.be.true; 40 | }); 41 | 42 | it("returns false if object has no registered handler functions", function () { 43 | const handler = sinon.spy(); 44 | expect(this.myClassInstance.canHandleCommand(this.myCommandInstance)).to.be.false; 45 | }); 46 | 47 | }); 48 | 49 | describe("register", function () { 50 | 51 | it("registers the provided function to handle the specified command sent through the command bus", function () { 52 | const handler = sinon.spy(); 53 | this.myClassInstance.register(MyCommand, handler); 54 | expect(this.myClassInstance.commandBus.hasHandlerFor(this.myCommandInstance)).to.be.true; 55 | }); 56 | 57 | }); 58 | 59 | describe("handle", function () { 60 | 61 | it("calls the handler when passed a command it can handle", function () { 62 | const handler = sinon.spy(); 63 | this.myClassInstance.register(MyCommand, handler); 64 | this.myClassInstance.handle(this.myCommandInstance); 65 | expect(handler).to.have.been.called; 66 | }); 67 | 68 | it("throws an error if the command cannot be handled", function () { 69 | const createWithoutHandlers = function () { 70 | this.myClassInstance.handle(this.myCommandInstance); 71 | }; 72 | expect(createWithoutHandlers).to.throw.error; 73 | }); 74 | 75 | }); 76 | 77 | }); 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /tests/unit/mixins/event-subscribing.tests.js: -------------------------------------------------------------------------------- 1 | 2 | describe("Space.messaging.EventSubscribing", function() { 3 | 4 | const MyClass = Space.Object.extend({ 5 | mixin: Space.messaging.EventSubscribing 6 | }); 7 | const MyEvent = Space.messaging.Event.extend( 8 | 'Space.messaging.EventSubscribing.__Test__.MyEvent', 9 | {} 10 | ); 11 | 12 | it("does not provide empty default", function() { 13 | expect(MyClass.prototype.eventSubscriptions).not.to.exist; 14 | }); 15 | 16 | it("does not throw error if no handlers have been defined", function() { 17 | const createWithoutHandlers = function() { 18 | new MyClass({ underscore: _ }).onDependenciesReady(); 19 | }; 20 | expect(createWithoutHandlers).not.to.throw(Error); 21 | }); 22 | 23 | describe("public methods", function() { 24 | 25 | beforeEach(function() { 26 | this.myClassInstance = new MyClass({ 27 | meteor: Meteor, 28 | eventBus: new Space.messaging.EventBus, 29 | underscore: _ 30 | }); 31 | this.myEventInstance = new MyEvent(); 32 | }); 33 | 34 | describe("canHandleEvent", function() { 35 | 36 | it("returns true if object has a subscribed handler function", function () { 37 | const handler = sinon.spy(); 38 | this.myClassInstance.subscribe(MyEvent, handler); 39 | expect(this.myClassInstance.canHandleEvent(this.myEventInstance)).to.be.true; 40 | }); 41 | 42 | it("returns false if object has no subscribed handler functions", function () { 43 | const handler = sinon.spy(); 44 | expect(this.myClassInstance.canHandleEvent(this.myEventInstance)).to.be.false; 45 | }); 46 | 47 | }); 48 | 49 | describe("subscribe", function() { 50 | 51 | it("subscribes the provided function to handle the specified event sent through the event bus", function () { 52 | const handler = sinon.spy(); 53 | this.myClassInstance.subscribe(MyEvent, handler); 54 | expect(this.myClassInstance.eventBus.hasHandlerFor(this.myEventInstance)).to.be.true; 55 | }); 56 | 57 | }); 58 | 59 | describe("on", function(){ 60 | 61 | it("calls the handler when passed an event it can handle", function() { 62 | const handler = sinon.spy(); 63 | this.myClassInstance.subscribe(MyEvent, handler); 64 | this.myClassInstance.on(this.myEventInstance); 65 | expect(handler).to.have.been.called; 66 | }); 67 | 68 | it("throws an error if the event cannot be handled", function() { 69 | const createWithoutHandlers = function() { 70 | this.myClassInstance.on(this.myEventInstance); 71 | }; 72 | expect(createWithoutHandlers).to.throw.error; 73 | }); 74 | 75 | }); 76 | 77 | }); 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /tests/unit/mixins/versionable.tests.js: -------------------------------------------------------------------------------- 1 | describe("Space.messaging.Versionable", function() { 2 | 3 | const MyVersionableClass = Space.Object.extend({ 4 | mixin: Space.messaging.Versionable, 5 | schemaVersion: 3, 6 | transformFromVersion1(data) { data.first = 'first'; }, 7 | transformFromVersion2(data) { data.second = 'second'; } 8 | }); 9 | 10 | it('is version 1 by default', function() { 11 | let MyVersionable = Space.Object.extend({ mixin: Space.messaging.Versionable }); 12 | let instance = MyVersionable.create(); 13 | expect(instance.schemaVersion).to.equal(1); 14 | }); 15 | 16 | it('can be transformed from older versions', function() { 17 | const originalData = { schemaVersion: 1 }; 18 | let instance = new MyVersionableClass(originalData); 19 | expect(instance.first).to.equal('first'); 20 | expect(instance.second).to.equal('second'); 21 | }); 22 | 23 | it('supports EJSON', function() { 24 | let instance = new MyVersionableClass({ schemaVersion: 1 }); 25 | let copy = EJSON.parse(EJSON.stringify(instance)); 26 | expect(copy.schemaVersion).to.equal(MyVersionableClass.prototype.schemaVersion); 27 | }); 28 | 29 | it("throws an error if a transform method is missing", function() { 30 | let MyVersionable = Space.Object.extend({ 31 | mixin: Space.messaging.Versionable, 32 | schemaVersion: 3, 33 | transformFromVersion1() {} 34 | }); 35 | const createVersionableWithoutTransformation = function() { 36 | return new MyVersionable({ schemaVersion: 1 }); 37 | }; 38 | expect(createVersionableWithoutTransformation).to.throw( 39 | MyVersionable.prototype.ERRORS.dataTransformMethodMissing(2) 40 | ); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/unit/serializable.unit.coffee: -------------------------------------------------------------------------------- 1 | 2 | Serializable = Space.messaging.Serializable 3 | test = Space.namespace 'Space.messaging.__test__' 4 | 5 | class test.MySerializable extends Serializable 6 | @type 'Space.messaging.__test__.MySerializable' 7 | fields: -> name: String, age: Match.Integer 8 | 9 | class test.MyNestedSerializable extends Serializable 10 | @type 'Space.messaging.__test__.MyNestedSerializable' 11 | fields: -> { 12 | single: test.MySerializable 13 | multiple: [test.MySerializable] 14 | } 15 | 16 | describe "Serializable", -> 17 | 18 | it 'is a test', -> 19 | expect(Serializable).to.extend Space.Struct 20 | 21 | describe 'construction', -> 22 | 23 | describe 'setting the type', -> 24 | 25 | class BasicTestType extends Serializable 26 | @type 'Space.messaging.BasicTestType' 27 | 28 | it 'makes the class EJSON serializable', -> 29 | instance = new BasicTestType() 30 | copy = EJSON.parse EJSON.stringify(instance) 31 | expect(instance).not.to.equal copy 32 | expect(copy).to.be.instanceof BasicTestType 33 | 34 | describe 'defining fields', -> 35 | 36 | class TestTypeWithFields extends Serializable 37 | @type 'Space.messaging.TestTypeWithFields' 38 | @fields: name: String, age: Match.Integer 39 | 40 | class TestTypeWithNestedTypes extends Serializable 41 | @type 'Space.messaging.TestTypeWithNestedTypes' 42 | @fields: sub: TestTypeWithFields 43 | 44 | it 'creates the instance and copies the fields over', -> 45 | instance = new TestTypeWithFields name: 'Dominik', age: 26 46 | copy = EJSON.parse EJSON.stringify(instance) 47 | 48 | expect(copy).to.instanceof TestTypeWithFields 49 | expect(instance.name).to.equal 'Dominik' 50 | expect(instance.age).to.equal 26 51 | expect(instance).to.deep.equal copy 52 | 53 | it 'handles sub types correctly', -> 54 | subType = new TestTypeWithFields name: 'Dominik', age: 26 55 | instance = new TestTypeWithNestedTypes sub: subType 56 | copy = EJSON.parse EJSON.stringify(instance) 57 | 58 | expect(instance.sub).to.equal subType 59 | expect(instance).to.deep.equal copy 60 | 61 | describe 'define serializables helper', -> 62 | 63 | Space.messaging.define Serializable, 'Space.messaging.__test__', 64 | SubType: type: String 65 | 66 | Space.messaging.define Serializable, 'Space.messaging.__test__', 67 | SuperType: 68 | sub: test.SubType 69 | 70 | it 'sets up serializables correctly', -> 71 | 72 | subType = new test.SubType type: 'test' 73 | instance = new test.SuperType sub: subType 74 | copy = EJSON.parse EJSON.stringify(instance) 75 | 76 | expect(instance.sub).to.be.instanceof test.SubType 77 | expect(instance.sub).to.equal subType 78 | expect(instance).to.be.instanceof test.SuperType 79 | expect(instance).to.deep.equal copy 80 | 81 | describe "serializing to and from plain object hierarchies", -> 82 | 83 | exampleNestedData = { 84 | _type: 'Space.messaging.__test__.MyNestedSerializable' 85 | single: { _type: 'Space.messaging.__test__.MySerializable', name: 'Test', age: 10 } 86 | multiple: [ 87 | { _type: 'Space.messaging.__test__.MySerializable', name: 'Bla', age: 2 } 88 | { _type: 'Space.messaging.__test__.MySerializable', name: 'Blub', age: 5 } 89 | ] 90 | } 91 | 92 | describe "::toData", -> 93 | 94 | it "returns a hierarchy of plain data objects", -> 95 | mySerializable = new test.MyNestedSerializable { 96 | single: new test.MySerializable(name: 'Test', age: 10) 97 | multiple: [ 98 | new test.MySerializable(name: 'Bla', age: 2) 99 | new test.MySerializable(name: 'Blub', age: 5) 100 | ] 101 | } 102 | expect(mySerializable.toData()).to.deep.equal exampleNestedData 103 | 104 | describe ".fromData", -> 105 | 106 | it "constructs the struct hierarchy from plain data object hierarchy", -> 107 | 108 | mySerializable = test.MyNestedSerializable.fromData exampleNestedData 109 | expect(mySerializable).to.be.instanceOf(test.MyNestedSerializable) 110 | expect(mySerializable.single).to.be.instanceOf(test.MySerializable) 111 | expect(mySerializable.multiple[0].toData()).to.deep.equal exampleNestedData.multiple[0] 112 | expect(mySerializable.multiple[1].toData()).to.deep.equal exampleNestedData.multiple[1] 113 | -------------------------------------------------------------------------------- /tests/unit/serializables/command.unit.coffee: -------------------------------------------------------------------------------- 1 | 2 | Command = Space.messaging.Command 3 | 4 | describe 'Space.messaging.Command', -> 5 | 6 | beforeEach -> 7 | @command = new Command 8 | 9 | it "is Ejsonable", -> 10 | expect(Command.hasMixin(Space.messaging.Ejsonable)).to.be.true 11 | 12 | it "is Versionable", -> 13 | expect(Command.hasMixin(Space.messaging.Versionable)).to.be.true 14 | 15 | it 'defines its EJSON type correctly', -> 16 | expect(@command.typeName()).to.equal 'Space.messaging.Command' 17 | -------------------------------------------------------------------------------- /tests/unit/serializables/event.unit.coffee: -------------------------------------------------------------------------------- 1 | 2 | Event = Space.messaging.Event 3 | 4 | describe 'Space.messaging.Event', -> 5 | 6 | beforeEach -> 7 | @event = new Event 8 | 9 | it "is Ejsonable", -> 10 | expect(Event.hasMixin(Space.messaging.Ejsonable)).to.be.true 11 | 12 | it "is Versionable", -> 13 | expect(Event.hasMixin(Space.messaging.Versionable)).to.be.true 14 | 15 | it 'defines its EJSON type correctly', -> 16 | expect(@event.typeName()).to.equal 'Space.messaging.Event' -------------------------------------------------------------------------------- /tests/unit/serializables/space-error.tests.js: -------------------------------------------------------------------------------- 1 | describe("Space.Error - messaging", function() { 2 | 3 | let MyCustomValue = Space.Struct.extend('MyCustomValue', { 4 | mixin: [Space.messaging.Ejsonable], 5 | statics: { fields: { value: String } } 6 | }); 7 | 8 | let MyCustomError = Space.Error.extend('MyCustomError', { 9 | statics: { fields: { custom: MyCustomValue } } 10 | }); 11 | 12 | it("is Versionable", function() { 13 | expect(MyCustomError.hasMixin(Space.messaging.Versionable)).to.be.true; 14 | }); 15 | 16 | it("is Ejsonable", function() { 17 | expect(MyCustomError.hasMixin(Space.messaging.Ejsonable)).to.be.true; 18 | }); 19 | 20 | it("makes Space.Error serializable", function() { 21 | let customValue = new MyCustomValue({ value: 'test' }); 22 | let error = new MyCustomError({ custom: customValue }); 23 | let copy = EJSON.parse(EJSON.stringify(error)); 24 | expect(copy).to.be.instanceof(MyCustomError); 25 | expect(copy.custom).to.be.instanceof(MyCustomValue); 26 | expect(copy.custom.value).to.equal(customValue.value); 27 | }); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /tests/unit/value-objects/guid.unit.coffee: -------------------------------------------------------------------------------- 1 | describe "Guid", -> 2 | 3 | # =============== CONSTRUCTION ================ # 4 | 5 | describe 'construction', -> 6 | 7 | it 'is serializable', -> 8 | guid = new Guid() 9 | copy = EJSON.parse EJSON.stringify(guid) 10 | expect(copy.equals(guid)).to.be.true; 11 | 12 | it 'generates a globally unique id', -> 13 | 14 | guid = new Guid() 15 | expect(guid.valueOf()).to.match Guid.REGEXP 16 | expect(guid.toString()).to.match Guid.REGEXP 17 | 18 | it 'optionally assigns a given id', -> 19 | 20 | id = '936DA01F-9ABD-4D9D-80C7-02AF85C822A8' 21 | guid = new Guid(id) 22 | expect(guid.valueOf()).to.equal id 23 | expect(guid.toString()).to.equal id 24 | 25 | it 'checks given id to be compliant', -> 26 | expect(-> new Guid('123')).to.throw() 27 | 28 | # =============== EQUALITY ================ # 29 | 30 | describe 'equality', -> 31 | 32 | it 'is equal when ids match', -> 33 | 34 | guid1 = new Guid() 35 | guid2 = new Guid guid1 36 | expect(guid1.equals(guid2)).to.be.true 37 | 38 | it 'is not equal when ids do not match', -> 39 | 40 | guid1 = new Guid() 41 | guid2 = new Guid() 42 | expect(guid1.equals(guid2)).to.be.false 43 | 44 | # =============== IMMUTABILITY ================ # 45 | 46 | describe 'immutability', -> 47 | 48 | it 'freezes itself', -> expect(Object.isFrozen(new Guid())).to.be.true 49 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "codeadventure:class", 5 | "1.0.0" 6 | ], 7 | [ 8 | "codeadventure:dependance", 9 | "1.0.0" 10 | ], 11 | [ 12 | "coffeescript", 13 | "1.0.4" 14 | ], 15 | [ 16 | "meteor", 17 | "1.1.3" 18 | ], 19 | [ 20 | "space:base", 21 | "1.1.0" 22 | ], 23 | [ 24 | "underscore", 25 | "1.0.1" 26 | ] 27 | ], 28 | "pluginDependencies": [], 29 | "toolVersion": "meteor-tool@1.0.35", 30 | "format": "1.0" 31 | } --------------------------------------------------------------------------------