Compare commits
95 Commits
Author | SHA1 | Date | |
---|---|---|---|
bd4897f392 | |||
dbdc8a2811 | |||
908d00981b | |||
669ef262d7 | |||
30053fe441 | |||
afb4e3339a | |||
e413a8116d | |||
ffeed0565c | |||
736240b978 | |||
73f4600c2a | |||
40beec1166 | |||
f8690fef50 | |||
972ddbf327 | |||
80aacd17a6 | |||
e67b3e50cc | |||
a4a8959b74 | |||
bab0f062f7 | |||
3bdfe4dcb4 | |||
fca960ad0d | |||
e43ed3951c | |||
23df304535 | |||
9a142175aa | |||
09b593e192 | |||
c27fc147b5 | |||
ddde21925a | |||
bd849d347d | |||
f2a85d4719 | |||
4e7c28ac83 | |||
243f1a70e9 | |||
b5a6517756 | |||
e12b128619 | |||
03fbab5265 | |||
1d13bf5bcc | |||
c2052f16a8 | |||
ff7cdc908c | |||
f3d41b8719 | |||
f9f0fc45e2 | |||
da6b7724b8 | |||
be7ca29e4b | |||
f401d78c4b | |||
6ceec0201f | |||
16ce4e09a9 | |||
2868ab686d | |||
5dab36382f | |||
02a32eb8c7 | |||
b258979b5a | |||
166e29bbf6 | |||
870f37d403 | |||
64c4b91678 | |||
f3e13292d8 | |||
7e1c405cb1 | |||
d1b4672eff | |||
0dd9fee52b | |||
37e1ee7970 | |||
bd0bb3acf5 | |||
f60497474e | |||
1d84cefa84 | |||
6792acd533 | |||
9397d89cf5 | |||
37cf4a91f4 | |||
52db86c929 | |||
e8f09c1b7a | |||
79edea873f | |||
97666a623d | |||
ef61ea9ad7 | |||
9c1504ef02 | |||
e8f2e04d1c | |||
e12aa7e961 | |||
857b7cd010 | |||
e100dea160 | |||
e8e87fcdba | |||
0d18b11721 | |||
eaaefddbe3 | |||
8c6946ddb6 | |||
3a7ebcdd80 | |||
ec2afbfd55 | |||
89feeca735 | |||
c4261765ec | |||
33fe6bcd41 | |||
1baf1c318c | |||
051aba3299 | |||
7998d79b13 | |||
6838a8729a | |||
67f4e33ca0 | |||
8a8277ae9f | |||
ff9cb9132c | |||
f4ce784a59 | |||
b34be4dcba | |||
6cc69efe2d | |||
8c30f294bc | |||
228eb791b7 | |||
057476ae66 | |||
cb80e4dc2e | |||
8410e09a4d | |||
eb04abddbf |
@ -1,16 +1,16 @@
|
|||||||
# gitzone ci_default
|
# gitzone ci_default
|
||||||
image: hosttoday/ht-docker-node:npmci
|
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
paths:
|
paths:
|
||||||
- .npmci_cache/
|
- .npmci_cache/
|
||||||
key: "$CI_BUILD_STAGE"
|
key: '$CI_BUILD_STAGE'
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- security
|
- security
|
||||||
- test
|
- test
|
||||||
- release
|
- release
|
||||||
- metadata
|
- metadata
|
||||||
|
|
||||||
# ====================
|
# ====================
|
||||||
# security stage
|
# security stage
|
||||||
@ -18,10 +18,11 @@ stages:
|
|||||||
mirror:
|
mirror:
|
||||||
stage: security
|
stage: security
|
||||||
script:
|
script:
|
||||||
- npmci git mirror
|
- npmci git mirror
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- lossless
|
||||||
- notpriv
|
- docker
|
||||||
|
- notpriv
|
||||||
|
|
||||||
snyk:
|
snyk:
|
||||||
stage: security
|
stage: security
|
||||||
@ -31,47 +32,51 @@ snyk:
|
|||||||
- npmci command npm install --ignore-scripts
|
- npmci command npm install --ignore-scripts
|
||||||
- npmci command snyk test
|
- npmci command snyk test
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- lossless
|
||||||
- notpriv
|
- docker
|
||||||
|
- notpriv
|
||||||
|
|
||||||
# ====================
|
# ====================
|
||||||
# test stage
|
# test stage
|
||||||
# ====================
|
# ====================
|
||||||
|
|
||||||
testLTS:
|
testStable:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- npmci npm prepare
|
- npmci npm prepare
|
||||||
- npmci node install lts
|
- npmci node install stable
|
||||||
- npmci npm install
|
- npmci npm install
|
||||||
- npmci npm test
|
- npmci npm test
|
||||||
coverage: /\d+.?\d+?\%\s*coverage/
|
coverage: /\d+.?\d+?\%\s*coverage/
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- lossless
|
||||||
- notpriv
|
- docker
|
||||||
|
- priv
|
||||||
testSTABLE:
|
|
||||||
|
testBuild:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- npmci npm prepare
|
- npmci npm prepare
|
||||||
- npmci node install stable
|
- npmci node install stable
|
||||||
- npmci npm install
|
- npmci npm install
|
||||||
- npmci npm test
|
- npmci command npm run build
|
||||||
coverage: /\d+.?\d+?\%\s*coverage/
|
coverage: /\d+.?\d+?\%\s*coverage/
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- lossless
|
||||||
- notpriv
|
- docker
|
||||||
|
- notpriv
|
||||||
|
|
||||||
release:
|
release:
|
||||||
stage: release
|
stage: release
|
||||||
script:
|
script:
|
||||||
- npmci node install stable
|
- npmci node install stable
|
||||||
- npmci npm publish
|
- npmci npm publish
|
||||||
only:
|
only:
|
||||||
- tags
|
- tags
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- lossless
|
||||||
- notpriv
|
- docker
|
||||||
|
- notpriv
|
||||||
|
|
||||||
# ====================
|
# ====================
|
||||||
# metadata stage
|
# metadata stage
|
||||||
@ -81,31 +86,35 @@ codequality:
|
|||||||
allow_failure: true
|
allow_failure: true
|
||||||
script:
|
script:
|
||||||
- npmci command npm install -g tslint typescript
|
- npmci command npm install -g tslint typescript
|
||||||
|
- npmci npm prepare
|
||||||
- npmci npm install
|
- npmci npm install
|
||||||
- npmci command "tslint -c tslint.json ./ts/**/*.ts"
|
- npmci command "tslint -c tslint.json ./ts/**/*.ts"
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- lossless
|
||||||
- priv
|
- docker
|
||||||
|
- priv
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
stage: metadata
|
stage: metadata
|
||||||
script:
|
script:
|
||||||
- npmci trigger
|
- npmci trigger
|
||||||
only:
|
only:
|
||||||
- tags
|
- tags
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- lossless
|
||||||
- notpriv
|
- docker
|
||||||
|
- notpriv
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
image: hosttoday/ht-docker-node:npmci
|
|
||||||
stage: metadata
|
stage: metadata
|
||||||
script:
|
script:
|
||||||
|
- npmci node install lts
|
||||||
- npmci command npm install -g @gitzone/tsdoc
|
- npmci command npm install -g @gitzone/tsdoc
|
||||||
- npmci npm prepare
|
- npmci npm prepare
|
||||||
- npmci npm install
|
- npmci npm install
|
||||||
- npmci command tsdoc
|
- npmci command tsdoc
|
||||||
tags:
|
tags:
|
||||||
|
- lossless
|
||||||
- docker
|
- docker
|
||||||
- notpriv
|
- notpriv
|
||||||
only:
|
only:
|
||||||
@ -113,5 +122,5 @@ pages:
|
|||||||
artifacts:
|
artifacts:
|
||||||
expire_in: 1 week
|
expire_in: 1 week
|
||||||
paths:
|
paths:
|
||||||
- public
|
- public
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
|
29
.vscode/launch.json
vendored
Normal file
29
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "current file",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"args": [
|
||||||
|
"${relativeFile}"
|
||||||
|
],
|
||||||
|
"runtimeArgs": ["-r", "@gitzone/tsrun"],
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
|
"protocol": "inspector",
|
||||||
|
"internalConsoleOptions": "openOnSessionStart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test.ts",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"args": [
|
||||||
|
"test/test.ts"
|
||||||
|
],
|
||||||
|
"runtimeArgs": ["-r", "@gitzone/tsrun"],
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
|
"protocol": "inspector",
|
||||||
|
"internalConsoleOptions": "openOnSessionStart"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
1164
package-lock.json
generated
1164
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
45
package.json
45
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pushrocks/smartuniverse",
|
"name": "@pushrocks/smartuniverse",
|
||||||
"version": "1.0.49",
|
"version": "1.0.97",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "messaging service for your micro services",
|
"description": "messaging service for your micro services",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
@ -14,35 +14,38 @@
|
|||||||
"format": "(gitzone format)"
|
"format": "(gitzone format)"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@gitzone/tsbuild": "^2.1.11",
|
"@gitzone/tsbuild": "^2.1.17",
|
||||||
"@gitzone/tstest": "^1.0.24",
|
"@gitzone/tstest": "^1.0.28",
|
||||||
"@pushrocks/tapbundle": "^3.0.9",
|
"@pushrocks/tapbundle": "^3.0.13",
|
||||||
"@types/node": "^12.0.6",
|
"@types/node": "^12.12.7",
|
||||||
"tslint": "^5.17.0",
|
"tslint": "^5.20.1",
|
||||||
"tslint-config-prettier": "^1.18.0"
|
"tslint-config-prettier": "^1.18.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"rxjs": "*"
|
"rxjs": "*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pushrocks/lik": "^3.0.5",
|
"@apiglobal/typedrequest-interfaces": "^1.0.7",
|
||||||
"@pushrocks/smartdelay": "^2.0.3",
|
"@pushrocks/lik": "^3.0.11",
|
||||||
"@pushrocks/smartexpress": "^3.0.21",
|
"@pushrocks/smartdelay": "^2.0.6",
|
||||||
"@pushrocks/smartfile": "^7.0.2",
|
"@pushrocks/smartexpress": "^3.0.52",
|
||||||
"@pushrocks/smarthash": "^2.0.4",
|
"@pushrocks/smartfile": "^7.0.6",
|
||||||
"@pushrocks/smartpromise": "^3.0.2",
|
"@pushrocks/smarthash": "^2.0.6",
|
||||||
"@pushrocks/smartrequest": "^1.1.15",
|
"@pushrocks/smartlog": "^2.0.21",
|
||||||
"@pushrocks/smartrx": "^2.0.3",
|
"@pushrocks/smartpromise": "^3.0.6",
|
||||||
"@pushrocks/smartsocket": "^1.1.37",
|
"@pushrocks/smartrequest": "^1.1.42",
|
||||||
"@pushrocks/smarttime": "^3.0.7",
|
"@pushrocks/smartrx": "^2.0.5",
|
||||||
|
"@pushrocks/smartsocket": "^1.1.58",
|
||||||
|
"@pushrocks/smarttime": "^3.0.12",
|
||||||
"@pushrocks/smartunique": "^3.0.1"
|
"@pushrocks/smartunique": "^3.0.1"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/*",
|
"ts/**/*",
|
||||||
"ts_web/*",
|
"ts_web/**/*",
|
||||||
"dist/*",
|
"dist/**/*",
|
||||||
"dist_web/*",
|
"dist_web/**/*",
|
||||||
"assets/*",
|
"dist_ts_web/**/*",
|
||||||
|
"assets/**/*",
|
||||||
"cli.js",
|
"cli.js",
|
||||||
"npmextra.json",
|
"npmextra.json",
|
||||||
"readme.md"
|
"readme.md"
|
||||||
|
@ -47,9 +47,13 @@ myUniverse.start(8765); // start the server and provide the port on which to lis
|
|||||||
|
|
||||||
All your microservices represents clients in the universe that may talk to each other using the universe server.
|
All your microservices represents clients in the universe that may talk to each other using the universe server.
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
|
||||||
|
|
||||||
For further information read the linked docs at the top of this readme.
|
For further information read the linked docs at the top of this readme.
|
||||||
|
|
||||||
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
||||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy.html)
|
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
||||||
|
|
||||||
[](https://maintainedby.lossless.com)
|
[](https://maintainedby.lossless.com)
|
||||||
|
79
test/test.ts
79
test/test.ts
@ -15,7 +15,7 @@ const testServerData = {
|
|||||||
|
|
||||||
const testChannelData = {
|
const testChannelData = {
|
||||||
channelName: 'awesomeTestChannel',
|
channelName: 'awesomeTestChannel',
|
||||||
channelPass: 'awesomeChannelPAss'
|
channelPass: 'awesomeChannelPass'
|
||||||
};
|
};
|
||||||
|
|
||||||
tap.test('first test', async () => {
|
tap.test('first test', async () => {
|
||||||
@ -31,28 +31,32 @@ tap.test('add a message to the SmartUniverse', async () => {
|
|||||||
// testing message handling
|
// testing message handling
|
||||||
tap.test('create smartuniverse client', async () => {
|
tap.test('create smartuniverse client', async () => {
|
||||||
testClientUniverse = new smartuniverse.ClientUniverse({
|
testClientUniverse = new smartuniverse.ClientUniverse({
|
||||||
serverAddress: testServerData.serverAddress
|
serverAddress: testServerData.serverAddress,
|
||||||
|
autoReconnect: true
|
||||||
});
|
});
|
||||||
expect(testClientUniverse).to.be.instanceof(smartuniverse.ClientUniverse);
|
expect(testClientUniverse).to.be.instanceof(smartuniverse.ClientUniverse);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should add a channel to the universe', async () => {
|
tap.test('should add a channel to the universe', async () => {
|
||||||
await testUniverse.addChannel(testChannelData.channelName, testChannelData.channelPass);
|
testUniverse.addChannel(testChannelData.channelName, testChannelData.channelPass);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should add the same channel to the client universe in the same way', async () => {
|
tap.test('should add the same channel to the client universe in the same way', async () => {
|
||||||
await testClientUniverse.addChannel(testChannelData.channelName, testChannelData.channelPass);
|
testClientUniverse.addChannel(testChannelData.channelName, testChannelData.channelPass);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should start the ClientUniverse', async () => {
|
||||||
|
await testClientUniverse.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should get a observable correctly', async () => {
|
tap.test('should get a observable correctly', async () => {
|
||||||
testClientChannel = await testClientUniverse.getChannel(testChannelData.channelName);
|
testClientChannel = testClientUniverse.getChannel(testChannelData.channelName);
|
||||||
expect(testClientChannel).to.be.instanceof(smartuniverse.ClientUniverseChannel);
|
expect(testClientChannel).to.be.instanceof(smartuniverse.ClientUniverseChannel);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should send a message correctly', async () => {
|
tap.test('should send a message correctly', async () => {
|
||||||
await testClientUniverse.sendMessage({
|
await testClientUniverse.getChannel(testChannelData.channelName).sendMessage({
|
||||||
messageText: 'hello',
|
messageText: 'hello'
|
||||||
targetChannelName: testChannelData.channelName
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -62,16 +66,67 @@ tap.test('universe should contain the sent message', async () => {
|
|||||||
|
|
||||||
tap.test('a second client should be able to subscibe', async () => {
|
tap.test('a second client should be able to subscibe', async () => {
|
||||||
testClientUniverse2 = new smartuniverse.ClientUniverse({
|
testClientUniverse2 = new smartuniverse.ClientUniverse({
|
||||||
serverAddress: testServerData.serverAddress
|
serverAddress: testServerData.serverAddress,
|
||||||
|
autoReconnect: true
|
||||||
});
|
});
|
||||||
|
|
||||||
testClientUniverse2.addChannel(testChannelData.channelName, testChannelData.channelPass);
|
testClientUniverse2.addChannel(testChannelData.channelName, testChannelData.channelPass);
|
||||||
|
await testClientUniverse2.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should receive a message correctly', async () => {});
|
tap.test('should receive a message correctly', async tools => {
|
||||||
|
const done = tools.defer();
|
||||||
|
const testChannel = testClientUniverse.getChannel(testChannelData.channelName);
|
||||||
|
const testChannel2 = testClientUniverse2.getChannel(testChannelData.channelName);
|
||||||
|
const subscription = testChannel2.subscribe(messageArg => {
|
||||||
|
if (messageArg.messageText === 'hellothere') {
|
||||||
|
console.log('Yay##########');
|
||||||
|
done.resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await testChannel.sendMessage({
|
||||||
|
messageText: 'hellothere'
|
||||||
|
});
|
||||||
|
await done.promise;
|
||||||
|
});
|
||||||
|
|
||||||
tap.test('should disconnect the client correctly', async () => {
|
interface IDemoReqRes {
|
||||||
testClientUniverse.close();
|
method: 'demo';
|
||||||
|
request: {
|
||||||
|
wowso: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
hereso: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
tap.test('ReactionRequest and ReactionResponse should work', async () => {
|
||||||
|
const reactionResponse = new smartuniverse.ReactionResponse<IDemoReqRes>({
|
||||||
|
channels: [testUniverse.getChannel(testChannelData.channelName)],
|
||||||
|
funcDef: async reqData => {
|
||||||
|
console.log(reqData);
|
||||||
|
return {
|
||||||
|
hereso: 'Hello there'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
method: 'demo'
|
||||||
|
});
|
||||||
|
const reactionRequest = new smartuniverse.ReactionRequest<IDemoReqRes>({
|
||||||
|
method: 'demo'
|
||||||
|
});
|
||||||
|
const reactionResult = await reactionRequest.fire(
|
||||||
|
[testClientUniverse2.getChannel(testChannelData.channelName)],
|
||||||
|
{
|
||||||
|
wowso: 'wowza'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const result = await reactionResult.getFirstResult();
|
||||||
|
console.log(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should disconnect the client correctly', async tools => {
|
||||||
|
await testClientUniverse.stop();
|
||||||
|
await testClientUniverse2.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should end the server correctly', async tools => {
|
tap.test('should end the server correctly', async tools => {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Client classes
|
// Client classes
|
||||||
export * from './smartuniverse.classes.clientuniverse';
|
export * from './smartuniverse.classes.clientuniverse';
|
||||||
export * from './smartuniverse.classes.clientuniversechannel';
|
export * from './smartuniverse.classes.clientuniversechannel';
|
||||||
|
export * from './smartuniverse.classes.clientuniversemessage';
|
||||||
|
|
||||||
// Server classes
|
// Server classes
|
||||||
export * from './smartuniverse.classes.universe';
|
export * from './smartuniverse.classes.universe';
|
||||||
@ -8,4 +9,8 @@ export * from './smartuniverse.classes.universecache';
|
|||||||
export * from './smartuniverse.classes.universechannel';
|
export * from './smartuniverse.classes.universechannel';
|
||||||
export * from './smartuniverse.classes.universemessage';
|
export * from './smartuniverse.classes.universemessage';
|
||||||
|
|
||||||
|
// Reaction Response
|
||||||
|
export * from './smartuniverse.classes.reactionrequest';
|
||||||
|
export * from './smartuniverse.classes.reactionresponse';
|
||||||
|
|
||||||
export * from './interfaces';
|
export * from './interfaces';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export * from './http.interfaces';
|
export * from './http.interfaces';
|
||||||
|
export * from './socketfunctionrequests';
|
||||||
export * from './universechannel.interfaces';
|
export * from './universechannel.interfaces';
|
||||||
export * from './universemessage.interfaces';
|
export * from './universemessage.interfaces';
|
||||||
export * from './universeactions.interfaces';
|
export * from './universeactions.interfaces';
|
||||||
|
20
ts/interfaces/socketfunctionrequests.ts
Normal file
20
ts/interfaces/socketfunctionrequests.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import * as interfaces from './index';
|
||||||
|
|
||||||
|
export interface ISocketRequest_SubscribeChannel {
|
||||||
|
method: 'subscribeChannel';
|
||||||
|
request: {
|
||||||
|
name: string;
|
||||||
|
passphrase: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
subscriptionStatus: 'subscribed' | 'unsubscribed';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISocketRequest_ProcessMessage {
|
||||||
|
method: 'processMessage';
|
||||||
|
request: interfaces.IUniverseMessage;
|
||||||
|
response: {
|
||||||
|
messageStatus: 'ok' | 'channel not found';
|
||||||
|
};
|
||||||
|
}
|
@ -1,13 +1,3 @@
|
|||||||
export type IServerCallActions = 'subscribe' | 'sendmessage' | 'unsubscribe';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the interface for a subscription
|
|
||||||
*/
|
|
||||||
export interface IServerCallSubscribeActionPayload {
|
|
||||||
name: string;
|
|
||||||
passphrase: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IServerUnsubscribeActionPayload {
|
export interface IServerUnsubscribeActionPayload {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
export interface IMessageCreator {
|
export interface IMessageCreator {
|
||||||
messageText: string;
|
messageText: string;
|
||||||
payload?: string | number | any;
|
payload?: string | number | any;
|
||||||
payloadStringType?: 'Buffer' | 'string' | 'object';
|
|
||||||
targetChannelName: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A universe
|
||||||
|
*/
|
||||||
export interface IUniverseMessage extends IMessageCreator {
|
export interface IUniverseMessage extends IMessageCreator {
|
||||||
id: string;
|
id: string;
|
||||||
/**
|
/**
|
||||||
@ -12,4 +13,5 @@ export interface IUniverseMessage extends IMessageCreator {
|
|||||||
*/
|
*/
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
passphrase: string;
|
passphrase: string;
|
||||||
|
targetChannelName: string;
|
||||||
}
|
}
|
||||||
|
8
ts/smartuniverse.classes.broadcastevent.ts
Normal file
8
ts/smartuniverse.classes.broadcastevent.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import * as plugins from './smartuniverse.plugins';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* broadcasts an event to multiple channels
|
||||||
|
*/
|
||||||
|
export class BroadcastEvent<T> {
|
||||||
|
fire() {}
|
||||||
|
}
|
5
ts/smartuniverse.classes.broadcastsubscription.ts
Normal file
5
ts/smartuniverse.classes.broadcastsubscription.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import * as plugins from './smartuniverse.plugins';
|
||||||
|
|
||||||
|
export class BroadcastSubscription {
|
||||||
|
|
||||||
|
}
|
@ -7,11 +7,12 @@ import * as url from 'url';
|
|||||||
|
|
||||||
import * as interfaces from './interfaces';
|
import * as interfaces from './interfaces';
|
||||||
|
|
||||||
import { ClientUniverseChannel, UniverseMessage } from './';
|
import { ClientUniverseChannel, ClientUniverseMessage } from './';
|
||||||
import { ClientUniverseCache } from './smartuniverse.classes.clientuniversecache';
|
import { ClientUniverseCache } from './smartuniverse.classes.clientuniversecache';
|
||||||
|
|
||||||
export interface IClientOptions {
|
export interface IClientOptions {
|
||||||
serverAddress: string;
|
serverAddress: string;
|
||||||
|
autoReconnect: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,13 +20,13 @@ export interface IClientOptions {
|
|||||||
* allows connecting to a universe server
|
* allows connecting to a universe server
|
||||||
*/
|
*/
|
||||||
export class ClientUniverse {
|
export class ClientUniverse {
|
||||||
public options;
|
public options: IClientOptions;
|
||||||
public smartsocketClient: plugins.smartsocket.SmartsocketClient;
|
public smartsocketClient: plugins.smartsocket.SmartsocketClient;
|
||||||
public observableIntake: plugins.smartrx.ObservableIntake<UniverseMessage>;
|
public messageRxjsSubject = new plugins.smartrx.rxjs.Subject<ClientUniverseMessage<any>>();
|
||||||
|
|
||||||
public channelStore = new Objectmap<ClientUniverseChannel>();
|
|
||||||
public clientUniverseCache = new ClientUniverseCache();
|
public clientUniverseCache = new ClientUniverseCache();
|
||||||
|
|
||||||
|
public autoReconnectStatus: 'on' | 'off' = 'off';
|
||||||
|
|
||||||
constructor(optionsArg: IClientOptions) {
|
constructor(optionsArg: IClientOptions) {
|
||||||
this.options = optionsArg;
|
this.options = optionsArg;
|
||||||
}
|
}
|
||||||
@ -34,15 +35,20 @@ export class ClientUniverse {
|
|||||||
* adds a channel to the channelcache
|
* adds a channel to the channelcache
|
||||||
* TODO: verify channel before adding it to the channel cache
|
* TODO: verify channel before adding it to the channel cache
|
||||||
*/
|
*/
|
||||||
public async addChannel(channelNameArg: string, passphraseArg: string) {
|
public addChannel(channelNameArg: string, passphraseArg: string) {
|
||||||
const existingChannel = await this.getChannel(channelNameArg);
|
const existingChannel = this.getChannel(channelNameArg);
|
||||||
|
|
||||||
if (existingChannel) {
|
if (existingChannel) {
|
||||||
throw new Error('channel exists');
|
throw new Error('channel exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
// lets create the channel
|
// lets create the channel
|
||||||
ClientUniverseChannel.createClientUniverseChannel(this, channelNameArg, passphraseArg);
|
const clientUniverseChannel = ClientUniverseChannel.createClientUniverseChannel(
|
||||||
|
this,
|
||||||
|
channelNameArg,
|
||||||
|
passphraseArg
|
||||||
|
);
|
||||||
|
return clientUniverseChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,9 +56,8 @@ export class ClientUniverse {
|
|||||||
* @param channelName
|
* @param channelName
|
||||||
* @param passphraseArg
|
* @param passphraseArg
|
||||||
*/
|
*/
|
||||||
public async getChannel(channelName: string): Promise<ClientUniverseChannel> {
|
public getChannel(channelName: string): ClientUniverseChannel {
|
||||||
await this.checkConnection();
|
const clientUniverseChannel = this.clientUniverseCache.channelMap.find(channel => {
|
||||||
const clientUniverseChannel = this.channelStore.find(channel => {
|
|
||||||
return channel.name === channelName;
|
return channel.name === channelName;
|
||||||
});
|
});
|
||||||
return clientUniverseChannel;
|
return clientUniverseChannel;
|
||||||
@ -63,28 +68,23 @@ export class ClientUniverse {
|
|||||||
* @param messageArg
|
* @param messageArg
|
||||||
*/
|
*/
|
||||||
public removeChannel(channelNameArg, notifyServer = true) {
|
public removeChannel(channelNameArg, notifyServer = true) {
|
||||||
const clientUniverseChannel = this.channelStore.findOneAndRemove(channelItemArg => {
|
const clientUniverseChannel = this.clientUniverseCache.channelMap.findOneAndRemove(
|
||||||
return channelItemArg.name === channelNameArg;
|
channelItemArg => {
|
||||||
});
|
return channelItemArg.name === channelNameArg;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public async start() {
|
||||||
* sends a message towards the server
|
if (this.options.autoReconnect) {
|
||||||
* @param messageArg
|
this.autoReconnectStatus = 'on';
|
||||||
*/
|
}
|
||||||
public async sendMessage(messageArg: interfaces.IMessageCreator) {
|
|
||||||
await this.checkConnection();
|
await this.checkConnection();
|
||||||
const requestBody: interfaces.IUniverseMessage = {
|
|
||||||
id: plugins.smartunique.shortId(),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
passphrase: (await this.getChannel(messageArg.targetChannelName)).passphrase,
|
|
||||||
...messageArg
|
|
||||||
};
|
|
||||||
// TODO: User websocket connection if available
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public close() {
|
public async stop() {
|
||||||
this.smartsocketClient.disconnect();
|
this.autoReconnectStatus = 'off';
|
||||||
|
await this.disconnect('triggered');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,36 +92,104 @@ export class ClientUniverse {
|
|||||||
* since password validation is done through other means, a connection should always be possible
|
* since password validation is done through other means, a connection should always be possible
|
||||||
*/
|
*/
|
||||||
private async checkConnection(): Promise<void> {
|
private async checkConnection(): Promise<void> {
|
||||||
if (!this.smartsocketClient && !this.observableIntake) {
|
if (!this.smartsocketClient) {
|
||||||
const parsedURL = url.parse(this.options.serverAddress);
|
const parsedURL = url.parse(this.options.serverAddress);
|
||||||
const socketConfig: plugins.smartsocket.ISmartsocketClientOptions = {
|
const socketConfig: plugins.smartsocket.ISmartsocketClientOptions = {
|
||||||
alias: process.env.SOCKET_ALIAS || 'someclient',
|
alias: 'universeclient',
|
||||||
password: 'UniverseClient',
|
password: 'UniverseClient',
|
||||||
port: parseInt(parsedURL.port, 10),
|
port: parseInt(parsedURL.port, 10),
|
||||||
role: 'UniverseClient',
|
role: 'UniverseClient',
|
||||||
url: parsedURL.protocol + '//' + parsedURL.hostname
|
url: parsedURL.protocol + '//' + parsedURL.hostname
|
||||||
};
|
};
|
||||||
console.log(socketConfig);
|
|
||||||
this.smartsocketClient = new SmartsocketClient(socketConfig);
|
this.smartsocketClient = new SmartsocketClient(socketConfig);
|
||||||
this.observableIntake = new plugins.smartrx.ObservableIntake();
|
|
||||||
|
this.smartsocketClient.eventSubject.subscribe(async eventArg => {
|
||||||
|
switch (eventArg) {
|
||||||
|
case 'disconnected':
|
||||||
|
this.disconnect('upstreamEvent');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// lets define some basic actions
|
// lets define some basic actions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* should handle a forced unsubscription by the server
|
* should handle a forced unsubscription by the server
|
||||||
*/
|
*/
|
||||||
const unsubscribe = new plugins.smartsocket.SocketFunction({
|
const socketFunctionUnsubscribe = new plugins.smartsocket.SocketFunction({
|
||||||
funcName: 'unsubscribe',
|
funcName: 'unsubscribe',
|
||||||
allowedRoles: [],
|
allowedRoles: [],
|
||||||
funcDef: async (data: interfaces.IServerUnsubscribeActionPayload) => {}
|
funcDef: async (dataArg: interfaces.IServerUnsubscribeActionPayload) => {
|
||||||
|
const channel = this.clientUniverseCache.channelMap.find(channelArg => {
|
||||||
|
return channelArg.name === dataArg.name;
|
||||||
|
});
|
||||||
|
if (channel) {
|
||||||
|
channel.unsubscribe();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* should handle a message reception
|
* handles message reception
|
||||||
*/
|
*/
|
||||||
const receiveMessage = async () => {};
|
const socketFunctionProcessMessage = new plugins.smartsocket.SocketFunction<
|
||||||
|
interfaces.ISocketRequest_ProcessMessage
|
||||||
|
>({
|
||||||
|
funcName: 'processMessage',
|
||||||
|
allowedRoles: [],
|
||||||
|
funcDef: async messageDescriptorArg => {
|
||||||
|
plugins.smartlog.defaultLogger.log('info', 'Got message from server');
|
||||||
|
const clientUniverseMessage = ClientUniverseMessage.createMessageFromMessageDescriptor(
|
||||||
|
messageDescriptorArg
|
||||||
|
);
|
||||||
|
this.messageRxjsSubject.next(clientUniverseMessage);
|
||||||
|
|
||||||
|
// lets find the corresponding channel
|
||||||
|
const targetChannel = this.getChannel(clientUniverseMessage.targetChannelName);
|
||||||
|
if (targetChannel) {
|
||||||
|
await targetChannel.emitMessageLocally(clientUniverseMessage);
|
||||||
|
return {
|
||||||
|
messageStatus: 'ok'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
messageStatus: 'channel not found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// add functions
|
||||||
|
this.smartsocketClient.addSocketFunction(socketFunctionUnsubscribe);
|
||||||
|
this.smartsocketClient.addSocketFunction(socketFunctionProcessMessage);
|
||||||
|
|
||||||
await this.smartsocketClient.connect();
|
await this.smartsocketClient.connect();
|
||||||
|
plugins.smartlog.defaultLogger.log('info', 'universe client connected successfully');
|
||||||
|
await this.clientUniverseCache.channelMap.forEach(async clientUniverseChannelArg => {
|
||||||
|
await clientUniverseChannelArg.populateSubscriptionToServer();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async disconnect(
|
||||||
|
reason: 'upstreamEvent' | 'triggered' = 'triggered',
|
||||||
|
tryReconnect = false
|
||||||
|
) {
|
||||||
|
const instructDisconnect = async () => {
|
||||||
|
if (this.smartsocketClient) {
|
||||||
|
const smartsocketToDisconnect = this.smartsocketClient;
|
||||||
|
this.smartsocketClient = null; // making sure the upstreamEvent does not interfere
|
||||||
|
await smartsocketToDisconnect.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (reason === 'triggered' && this.smartsocketClient) {
|
||||||
|
await instructDisconnect();
|
||||||
|
}
|
||||||
|
if (this.autoReconnectStatus === 'on' && reason === 'upstreamEvent') {
|
||||||
|
await instructDisconnect();
|
||||||
|
await plugins.smartdelay.delayForRandom(5000, 20000);
|
||||||
|
await this.checkConnection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import * as plugins from './smartuniverse.plugins';
|
import * as plugins from './smartuniverse.plugins';
|
||||||
|
import { ClientUniverseChannel } from './smartuniverse.classes.clientuniversechannel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a cache for clients
|
* a cache for clients
|
||||||
* keeps track of which messages have already been received
|
* keeps track of which messages have already been received
|
||||||
* good for deduplication in mesh environments
|
* good for deduplication in mesh environments
|
||||||
*/
|
*/
|
||||||
export class ClientUniverseCache {}
|
export class ClientUniverseCache {
|
||||||
|
public channelMap = new plugins.lik.Objectmap<ClientUniverseChannel>();
|
||||||
|
}
|
||||||
|
@ -2,6 +2,9 @@ import * as plugins from './smartuniverse.plugins';
|
|||||||
import * as interfaces from './interfaces';
|
import * as interfaces from './interfaces';
|
||||||
|
|
||||||
import { ClientUniverse } from './';
|
import { ClientUniverse } from './';
|
||||||
|
import { ClientUniverseMessage } from './smartuniverse.classes.clientuniversemessage';
|
||||||
|
import { ReactionRequest } from './smartuniverse.classes.reactionrequest';
|
||||||
|
import { ReactionResponse } from './smartuniverse.classes.reactionresponse';
|
||||||
|
|
||||||
export class ClientUniverseChannel implements interfaces.IUniverseChannel {
|
export class ClientUniverseChannel implements interfaces.IUniverseChannel {
|
||||||
// ======
|
// ======
|
||||||
@ -13,18 +16,17 @@ export class ClientUniverseChannel implements interfaces.IUniverseChannel {
|
|||||||
* @param channelNameArg
|
* @param channelNameArg
|
||||||
* @param passphraseArg
|
* @param passphraseArg
|
||||||
*/
|
*/
|
||||||
public static async createClientUniverseChannel(
|
public static createClientUniverseChannel(
|
||||||
clientUniverseArg: ClientUniverse,
|
clientUniverseArg: ClientUniverse,
|
||||||
channelNameArg: string,
|
channelNameArg: string,
|
||||||
passphraseArg: string
|
passphraseArg: string
|
||||||
): Promise<ClientUniverseChannel> {
|
): ClientUniverseChannel {
|
||||||
const clientChannel = new ClientUniverseChannel(
|
const clientChannel = new ClientUniverseChannel(
|
||||||
clientUniverseArg,
|
clientUniverseArg,
|
||||||
channelNameArg,
|
channelNameArg,
|
||||||
passphraseArg
|
passphraseArg
|
||||||
);
|
);
|
||||||
clientUniverseArg.channelStore.add(clientChannel);
|
clientUniverseArg.clientUniverseCache.channelMap.add(clientChannel);
|
||||||
await clientChannel.subscribe();
|
|
||||||
return clientChannel;
|
return clientChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,12 +37,14 @@ export class ClientUniverseChannel implements interfaces.IUniverseChannel {
|
|||||||
// properties
|
// properties
|
||||||
public name: string;
|
public name: string;
|
||||||
public passphrase: string;
|
public passphrase: string;
|
||||||
|
public status: 'subscribed' | 'unsubscribed' = 'unsubscribed';
|
||||||
|
private subject = new plugins.smartrx.rxjs.Subject<ClientUniverseMessage<any>>();
|
||||||
|
|
||||||
// refs
|
// refs
|
||||||
public clientUniverse: ClientUniverse;
|
public clientUniverseRef: ClientUniverse;
|
||||||
|
|
||||||
constructor(clientUniverseArg: ClientUniverse, nameArg: string, passphraseArg: string) {
|
constructor(clientUniverseArg: ClientUniverse, nameArg: string, passphraseArg: string) {
|
||||||
this.clientUniverse = clientUniverseArg;
|
this.clientUniverseRef = clientUniverseArg;
|
||||||
this.name = nameArg;
|
this.name = nameArg;
|
||||||
this.passphrase = passphraseArg;
|
this.passphrase = passphraseArg;
|
||||||
}
|
}
|
||||||
@ -49,12 +53,53 @@ export class ClientUniverseChannel implements interfaces.IUniverseChannel {
|
|||||||
* subscribes to a channel
|
* subscribes to a channel
|
||||||
* tells the universe about this instances interest into a channel
|
* tells the universe about this instances interest into a channel
|
||||||
*/
|
*/
|
||||||
public async subscribe() {
|
public subscribe(observingFunctionArg: (messageArg: ClientUniverseMessage<any>) => void) {
|
||||||
const serverCallActionName: interfaces.IServerCallActions = 'subscribe';
|
return this.subject.subscribe(
|
||||||
const serverCallActionPayload: interfaces.IServerCallSubscribeActionPayload = {
|
messageArg => {
|
||||||
name: this.name,
|
observingFunctionArg(messageArg);
|
||||||
passphrase: this.passphrase
|
},
|
||||||
|
error => console.log(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribe() {
|
||||||
|
// TODO: unsubscribe all users
|
||||||
|
}
|
||||||
|
|
||||||
|
public async populateSubscriptionToServer() {
|
||||||
|
// lets make sure the channel is connected
|
||||||
|
if (this.status === 'unsubscribed') {
|
||||||
|
const response = await this.clientUniverseRef.smartsocketClient.serverCall<
|
||||||
|
interfaces.ISocketRequest_SubscribeChannel
|
||||||
|
>('subscribeChannel', {
|
||||||
|
name: this.name,
|
||||||
|
passphrase: this.passphrase
|
||||||
|
});
|
||||||
|
this.status = response.subscriptionStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async emitMessageLocally(messageArg: ClientUniverseMessage<any>) {
|
||||||
|
this.subject.next(messageArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sends a message towards the server
|
||||||
|
* @param messageArg
|
||||||
|
*/
|
||||||
|
public async sendMessage(messageArg: interfaces.IMessageCreator) {
|
||||||
|
await this.clientUniverseRef.start(); // its ok to call this multiple times
|
||||||
|
const universeMessageToSend: interfaces.IUniverseMessage = {
|
||||||
|
id: plugins.smartunique.shortId(),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
passphrase: this.passphrase,
|
||||||
|
targetChannelName: this.name,
|
||||||
|
messageText: messageArg.messageText,
|
||||||
|
payload: messageArg.payload
|
||||||
};
|
};
|
||||||
this.clientUniverse.smartsocketClient.serverCall(serverCallActionName, serverCallActionPayload);
|
await this.clientUniverseRef.smartsocketClient.serverCall(
|
||||||
|
'processMessage',
|
||||||
|
universeMessageToSend
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import * as plugins from './smartuniverse.plugins';
|
import * as plugins from './smartuniverse.plugins';
|
||||||
|
|
||||||
import * as interfaces from './interfaces';
|
import * as interfaces from './interfaces';
|
||||||
import { IUniverseMessage } from './interfaces';
|
|
||||||
|
|
||||||
export class ClientUniverseMessage implements interfaces.IUniverseMessage {
|
export class ClientUniverseMessage<T> implements interfaces.IUniverseMessage {
|
||||||
// ======
|
// ======
|
||||||
// STATIC
|
// STATIC
|
||||||
// ======
|
// ======
|
||||||
public static createMessageFromPayload(messageDescriptor: interfaces.IUniverseMessage) {}
|
public static createMessageFromMessageDescriptor(messageDescriptor: interfaces.IUniverseMessage) {
|
||||||
|
const clientuniverseMessage = new ClientUniverseMessage(messageDescriptor);
|
||||||
|
return clientuniverseMessage;
|
||||||
|
}
|
||||||
|
|
||||||
// ========
|
// ========
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@ -20,15 +22,17 @@ export class ClientUniverseMessage implements interfaces.IUniverseMessage {
|
|||||||
public smartTimestamp: plugins.smarttime.TimeStamp;
|
public smartTimestamp: plugins.smarttime.TimeStamp;
|
||||||
public messageText: string;
|
public messageText: string;
|
||||||
public passphrase: string;
|
public passphrase: string;
|
||||||
public payload: any;
|
public payload: T;
|
||||||
public payloadStringType;
|
|
||||||
public targetChannelName: string;
|
public targetChannelName: string;
|
||||||
|
|
||||||
constructor(messageArg: IUniverseMessage, payloadArg) {
|
constructor(messageArg: interfaces.IUniverseMessage) {
|
||||||
for (const key of Object.keys(messageArg)) {
|
for (const key of Object.keys(messageArg)) {
|
||||||
this[key] = messageArg[key];
|
this[key] = messageArg[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gets json for payload
|
||||||
|
*/
|
||||||
getAsJsonForPayload() {}
|
getAsJsonForPayload() {}
|
||||||
}
|
}
|
||||||
|
83
ts/smartuniverse.classes.reactionrequest.ts
Normal file
83
ts/smartuniverse.classes.reactionrequest.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import * as plugins from './smartuniverse.plugins';
|
||||||
|
import { UniverseChannel } from './smartuniverse.classes.universechannel';
|
||||||
|
import { ClientUniverseChannel } from './smartuniverse.classes.clientuniversechannel';
|
||||||
|
import { ReactionResult } from './smartuniverse.classes.reactionresult';
|
||||||
|
import { UniverseMessage } from './smartuniverse.classes.universemessage';
|
||||||
|
import { ClientUniverseMessage } from './smartuniverse.classes.clientuniversemessage';
|
||||||
|
|
||||||
|
export interface IReactionRequestConstructorOptions<
|
||||||
|
T extends plugins.typedrequestInterfaces.ITypedRequest
|
||||||
|
> {
|
||||||
|
method: T['method'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICombinatorPayload<T extends plugins.typedrequestInterfaces.ITypedRequest> {
|
||||||
|
/**
|
||||||
|
* needed for tying responses to requests
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
typedRequestPayload: {
|
||||||
|
method: T['method'];
|
||||||
|
request: T['request'];
|
||||||
|
response: T['response'];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReactionRequest<T extends plugins.typedrequestInterfaces.ITypedRequest> {
|
||||||
|
public method: T['method'];
|
||||||
|
|
||||||
|
constructor(optionsArg: IReactionRequestConstructorOptions<T>) {
|
||||||
|
this.method = optionsArg.method;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fire(
|
||||||
|
channelsArg: Array<UniverseChannel | ClientUniverseChannel>,
|
||||||
|
requestDataArg: T['request'],
|
||||||
|
timeoutMillisArg = 5000
|
||||||
|
) {
|
||||||
|
const subscriptionMap = new plugins.lik.Objectmap<plugins.smartrx.rxjs.Subscription>();
|
||||||
|
const reactionResult = new ReactionResult<T>();
|
||||||
|
const requestId = plugins.smartunique.shortId();
|
||||||
|
for (const channel of channelsArg) {
|
||||||
|
subscriptionMap.add(
|
||||||
|
channel.subscribe(
|
||||||
|
(
|
||||||
|
messageArg:
|
||||||
|
| UniverseMessage<ICombinatorPayload<T>>
|
||||||
|
| ClientUniverseMessage<ICombinatorPayload<T>>
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
messageArg.messageText === 'reactionResponse' &&
|
||||||
|
messageArg.payload.typedRequestPayload.method === this.method
|
||||||
|
) {
|
||||||
|
const payload: ICombinatorPayload<T> = messageArg.payload;
|
||||||
|
if (payload.id !== requestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reactionResult.pushReactionResponse(payload.typedRequestPayload.response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const payload: ICombinatorPayload<T> = {
|
||||||
|
id: requestId,
|
||||||
|
typedRequestPayload: {
|
||||||
|
method: this.method,
|
||||||
|
request: requestDataArg,
|
||||||
|
response: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
channel.sendMessage({
|
||||||
|
messageText: 'reactionRequest',
|
||||||
|
payload
|
||||||
|
});
|
||||||
|
}
|
||||||
|
plugins.smartdelay.delayFor(timeoutMillisArg).then(async () => {
|
||||||
|
await subscriptionMap.forEach(subscriptionArg => {
|
||||||
|
subscriptionArg.unsubscribe();
|
||||||
|
});
|
||||||
|
reactionResult.complete();
|
||||||
|
});
|
||||||
|
return reactionResult;
|
||||||
|
}
|
||||||
|
}
|
63
ts/smartuniverse.classes.reactionresponse.ts
Normal file
63
ts/smartuniverse.classes.reactionresponse.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import * as plugins from './smartuniverse.plugins';
|
||||||
|
|
||||||
|
import { ICombinatorPayload } from './smartuniverse.classes.reactionrequest';
|
||||||
|
import { UniverseChannel } from './smartuniverse.classes.universechannel';
|
||||||
|
import { ClientUniverseChannel } from './smartuniverse.classes.clientuniversechannel';
|
||||||
|
import { UniverseMessage } from './smartuniverse.classes.universemessage';
|
||||||
|
import { ClientUniverseMessage } from './smartuniverse.classes.clientuniversemessage';
|
||||||
|
|
||||||
|
export type TReactionResponseFuncDef<T extends plugins.typedrequestInterfaces.ITypedRequest> = (
|
||||||
|
dataArg: T['request']
|
||||||
|
) => Promise<T['response']>;
|
||||||
|
|
||||||
|
export interface IReactionResponseConstructorOptions<
|
||||||
|
T extends plugins.typedrequestInterfaces.ITypedRequest
|
||||||
|
> {
|
||||||
|
method: T['method'];
|
||||||
|
channels: Array<UniverseChannel | ClientUniverseChannel>;
|
||||||
|
funcDef: TReactionResponseFuncDef<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReactionResponse<T extends plugins.typedrequestInterfaces.ITypedRequest> {
|
||||||
|
public method: T['method'];
|
||||||
|
public channels = new plugins.lik.Objectmap<UniverseChannel | ClientUniverseChannel>();
|
||||||
|
public funcDef: TReactionResponseFuncDef<T>;
|
||||||
|
|
||||||
|
constructor(optionsArg: IReactionResponseConstructorOptions<T>) {
|
||||||
|
this.method = optionsArg.method;
|
||||||
|
this.channels.addArray(optionsArg.channels);
|
||||||
|
this.funcDef = optionsArg.funcDef;
|
||||||
|
for (const channel of this.channels.getArray()) {
|
||||||
|
channel.subscribe(messageArg => {
|
||||||
|
this.processMessageForReaction(channel, messageArg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processMessageForReaction(
|
||||||
|
channelArg: UniverseChannel | ClientUniverseChannel,
|
||||||
|
messageArg:
|
||||||
|
| UniverseMessage<ICombinatorPayload<T>>
|
||||||
|
| ClientUniverseMessage<ICombinatorPayload<T>>
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
messageArg.messageText === 'reactionRequest' &&
|
||||||
|
messageArg.payload.typedRequestPayload.method === this.method
|
||||||
|
) {
|
||||||
|
const response: T['response'] = await this.funcDef(
|
||||||
|
messageArg.payload.typedRequestPayload.request
|
||||||
|
);
|
||||||
|
const payload: ICombinatorPayload<T> = {
|
||||||
|
...messageArg.payload,
|
||||||
|
typedRequestPayload: {
|
||||||
|
...messageArg.payload.typedRequestPayload,
|
||||||
|
response
|
||||||
|
}
|
||||||
|
};
|
||||||
|
channelArg.sendMessage({
|
||||||
|
messageText: 'reactionResponse',
|
||||||
|
payload
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
ts/smartuniverse.classes.reactionresult.ts
Normal file
52
ts/smartuniverse.classes.reactionresult.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import * as plugins from './smartuniverse.plugins';
|
||||||
|
import { ReactionResponse } from './smartuniverse.classes.reactionresponse';
|
||||||
|
|
||||||
|
export class ReactionResult<T extends plugins.typedrequestInterfaces.ITypedRequest> {
|
||||||
|
private resultReplaySubject = new plugins.smartrx.rxjs.ReplaySubject<T['response']>();
|
||||||
|
private endResult: Array<T['response']> = [];
|
||||||
|
private completeDeferred = plugins.smartpromise.defer<Array<T['response']>>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.resultSubscribe(responseArg => {
|
||||||
|
this.endResult.push(responseArg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public resultSubscribe(observerArg: (responseArg: T['response']) => void) {
|
||||||
|
return this.resultReplaySubject.subscribe(observerArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gets the end result as an array of all results
|
||||||
|
*/
|
||||||
|
public async getEndResult() {
|
||||||
|
const result = await this.completeDeferred.promise;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if there is a single respondant, or you are only interested in the first result
|
||||||
|
*/
|
||||||
|
public async getFirstResult() {
|
||||||
|
const done = plugins.smartpromise.defer<T['response']>();
|
||||||
|
const subscription = this.resultReplaySubject.subscribe(result => {
|
||||||
|
done.resolve(result);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
return await done.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* push a reactionResponse
|
||||||
|
*/
|
||||||
|
public async pushReactionResponse(responseArg: T['response']) {
|
||||||
|
this.resultReplaySubject.next(responseArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* completes the ReactionResult
|
||||||
|
*/
|
||||||
|
public async complete() {
|
||||||
|
this.completeDeferred.resolve(this.endResult);
|
||||||
|
}
|
||||||
|
}
|
@ -6,20 +6,19 @@ import { UniverseCache, UniverseChannel, UniverseMessage } from './';
|
|||||||
import * as paths from './smartuniverse.paths';
|
import * as paths from './smartuniverse.paths';
|
||||||
|
|
||||||
import * as interfaces from './interfaces';
|
import * as interfaces from './interfaces';
|
||||||
import { UniverseConnectionManager } from './smartuniverse.classes.universeconnectionmanager';
|
|
||||||
import { UniverseConnection } from './smartuniverse.classes.universeconnection';
|
import { UniverseConnection } from './smartuniverse.classes.universeconnection';
|
||||||
|
|
||||||
export interface ISmartUniverseConstructorOptions {
|
export interface ISmartUniverseConstructorOptions {
|
||||||
messageExpiryInMilliseconds: number;
|
messageExpiryInMilliseconds: number;
|
||||||
|
externalServer?: plugins.smartexpress.Server;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* main class that setsup a Universe
|
* main class that setups a Universe
|
||||||
*/
|
*/
|
||||||
export class Universe {
|
export class Universe {
|
||||||
// subinstances
|
// subinstances
|
||||||
public universeCache: UniverseCache;
|
public universeCache: UniverseCache;
|
||||||
public universeConnectionManager: UniverseConnectionManager;
|
|
||||||
|
|
||||||
// options
|
// options
|
||||||
private options: ISmartUniverseConstructorOptions;
|
private options: ISmartUniverseConstructorOptions;
|
||||||
@ -36,8 +35,7 @@ export class Universe {
|
|||||||
|
|
||||||
constructor(optionsArg: ISmartUniverseConstructorOptions) {
|
constructor(optionsArg: ISmartUniverseConstructorOptions) {
|
||||||
this.options = optionsArg;
|
this.options = optionsArg;
|
||||||
this.universeCache = new UniverseCache(this.options.messageExpiryInMilliseconds);
|
this.universeCache = new UniverseCache(this, this.options.messageExpiryInMilliseconds);
|
||||||
this.universeConnectionManager = new UniverseConnectionManager();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,8 +60,18 @@ export class Universe {
|
|||||||
/**
|
/**
|
||||||
* adds a channel to the Universe
|
* adds a channel to the Universe
|
||||||
*/
|
*/
|
||||||
public async addChannel(nameArg: string, passphraseArg: string) {
|
public addChannel(nameArg: string, passphraseArg: string) {
|
||||||
const newChannel = UniverseChannel.createChannel(this.universeCache, nameArg, passphraseArg);
|
const newChannel = UniverseChannel.createChannel(this, nameArg, passphraseArg);
|
||||||
|
return newChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns a channel
|
||||||
|
*/
|
||||||
|
public getChannel(channelNameArg: string) {
|
||||||
|
return this.universeCache.channelMap.find(channelArg => {
|
||||||
|
return channelArg.name === channelNameArg;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,14 +79,19 @@ export class Universe {
|
|||||||
*/
|
*/
|
||||||
public async start(portArg: number) {
|
public async start(portArg: number) {
|
||||||
// lets create the base smartexpress server
|
// lets create the base smartexpress server
|
||||||
this.smartexpressServer = new plugins.smartexpress.Server({
|
if (!this.options.externalServer) {
|
||||||
cors: true,
|
this.smartexpressServer = new plugins.smartexpress.Server({
|
||||||
defaultAnswer: async () => {
|
cors: true,
|
||||||
return `smartuniverse server ${this.getUniverseVersion()}`;
|
defaultAnswer: async () => {
|
||||||
},
|
return `smartuniverse server ${this.getUniverseVersion()}`;
|
||||||
forceSsl: false,
|
},
|
||||||
port: portArg
|
forceSsl: false,
|
||||||
});
|
port: portArg
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('Universe is using externally supplied server');
|
||||||
|
this.smartexpressServer = this.options.externalServer;
|
||||||
|
}
|
||||||
|
|
||||||
// add websocket upgrade
|
// add websocket upgrade
|
||||||
this.smartsocket = new plugins.smartsocket.Smartsocket({});
|
this.smartsocket = new plugins.smartsocket.Smartsocket({});
|
||||||
@ -92,27 +105,74 @@ export class Universe {
|
|||||||
// add the role to smartsocket
|
// add the role to smartsocket
|
||||||
this.smartsocket.addSocketRoles([ClientRole]);
|
this.smartsocket.addSocketRoles([ClientRole]);
|
||||||
|
|
||||||
const SubscriptionSocketFunction = new plugins.smartsocket.SocketFunction({
|
const socketFunctionSubscription = new plugins.smartsocket.SocketFunction<
|
||||||
|
interfaces.ISocketRequest_SubscribeChannel
|
||||||
|
>({
|
||||||
allowedRoles: [ClientRole], // there is only one client role, Authentication happens on another level
|
allowedRoles: [ClientRole], // there is only one client role, Authentication happens on another level
|
||||||
funcName: 'channelSubscription',
|
funcName: 'subscribeChannel',
|
||||||
funcDef: async (dataArg, socketConnectionArg) => {
|
funcDef: async (dataArg, socketConnectionArg) => {
|
||||||
// run in "this context" of this class
|
const universeConnection = new UniverseConnection({
|
||||||
(() => {
|
universe: this,
|
||||||
// TODO: properly add the connection
|
socketConnection: socketConnectionArg,
|
||||||
const universeConnection = new UniverseConnection({
|
authenticationRequests: [dataArg]
|
||||||
|
});
|
||||||
})
|
await UniverseConnection.addConnectionToCache(this, universeConnection);
|
||||||
this.universeConnectionManager.addConnection();
|
return {
|
||||||
})();
|
subscriptionStatus: 'subscribed'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const socketFunctionProcessMessage = new plugins.smartsocket.SocketFunction({
|
||||||
|
allowedRoles: [ClientRole], // there is only one client role, Authentication happens on another level
|
||||||
|
funcName: 'processMessage',
|
||||||
|
funcDef: async (dataArg: interfaces.IUniverseMessage, socketConnectionArg) => {
|
||||||
|
const universeConnection = UniverseConnection.findUniverseConnectionBySocketConnection(
|
||||||
|
this.universeCache,
|
||||||
|
socketConnectionArg
|
||||||
|
);
|
||||||
|
if (universeConnection) {
|
||||||
|
plugins.smartlog.defaultLogger.log(
|
||||||
|
'ok',
|
||||||
|
'found UniverseConnection for socket for incoming message'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
plugins.smartlog.defaultLogger.log(
|
||||||
|
'warn',
|
||||||
|
'found no Authorized channel for incoming message'
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
error: 'You need to authenticate for a channel'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const unauthenticatedMessage = UniverseMessage.createMessageFromPayload(
|
||||||
|
socketConnectionArg,
|
||||||
|
dataArg
|
||||||
|
);
|
||||||
|
const foundChannel = await UniverseChannel.authorizeAMessageForAChannel(
|
||||||
|
this.universeCache,
|
||||||
|
unauthenticatedMessage
|
||||||
|
);
|
||||||
|
if (foundChannel && unauthenticatedMessage.authenticated) {
|
||||||
|
const authenticatedMessage = unauthenticatedMessage;
|
||||||
|
await this.universeCache.addMessage(authenticatedMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// add socket functions
|
||||||
|
this.smartsocket.addSocketFunction(socketFunctionSubscription);
|
||||||
|
this.smartsocket.addSocketFunction(socketFunctionProcessMessage);
|
||||||
|
|
||||||
|
// start the server
|
||||||
|
if (!this.options.externalServer) {
|
||||||
|
await this.smartexpressServer.start();
|
||||||
|
}
|
||||||
|
|
||||||
// add smartsocket to the running smartexpress app
|
// add smartsocket to the running smartexpress app
|
||||||
this.smartsocket.setExternalServer('smartexpress', this.smartexpressServer as any);
|
await this.smartsocket.setExternalServer('smartexpress', this.smartexpressServer);
|
||||||
// start everything
|
|
||||||
await this.smartexpressServer.start();
|
|
||||||
await this.smartsocket.start();
|
await this.smartsocket.start();
|
||||||
console.log('started universe');
|
plugins.smartlog.defaultLogger.log('success', 'started universe');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -120,6 +180,8 @@ export class Universe {
|
|||||||
*/
|
*/
|
||||||
public async stopServer() {
|
public async stopServer() {
|
||||||
await this.smartsocket.stop();
|
await this.smartsocket.stop();
|
||||||
await this.smartexpressServer.stop();
|
if (!this.options.externalServer) {
|
||||||
|
await this.smartexpressServer.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ import { Objectmap } from '@pushrocks/lik';
|
|||||||
import { Observable, from } from 'rxjs';
|
import { Observable, from } from 'rxjs';
|
||||||
import { filter } from 'rxjs/operators';
|
import { filter } from 'rxjs/operators';
|
||||||
import { rxjs } from '@pushrocks/smartrx';
|
import { rxjs } from '@pushrocks/smartrx';
|
||||||
|
import { UniverseConnection } from './smartuniverse.classes.universeconnection';
|
||||||
|
import { Universe } from './smartuniverse.classes.universe';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* universe store handles the creation, storage and retrieval of messages.
|
* universe store handles the creation, storage and retrieval of messages.
|
||||||
@ -17,25 +19,34 @@ export class UniverseCache {
|
|||||||
// INSTANCE
|
// INSTANCE
|
||||||
// ========
|
// ========
|
||||||
public standardMessageExpiry: number;
|
public standardMessageExpiry: number;
|
||||||
public destructionTime: number = 60000;
|
public destructionTime: number = 10000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* stores messages for this instance
|
* stores messages for this instance
|
||||||
*/
|
*/
|
||||||
public messageMap = new Objectmap<UniverseMessage>();
|
public messageMap = new Objectmap<UniverseMessage<any>>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* stores the channels that are available within the universe
|
* stores the channels that are available within the universe
|
||||||
*/
|
*/
|
||||||
public channelMap = new Objectmap<UniverseChannel>();
|
public channelMap = new Objectmap<UniverseChannel>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stores all connections
|
||||||
|
*/
|
||||||
|
public connectionMap = new plugins.lik.Objectmap<UniverseConnection>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* allows messages to be processed in a blacklist mode for further analysis
|
* allows messages to be processed in a blacklist mode for further analysis
|
||||||
*/
|
*/
|
||||||
public blackListChannel = new UniverseChannel(this, 'blacklist', 'nada');
|
public blackListChannel: UniverseChannel;
|
||||||
|
|
||||||
constructor(standardMessageExpiryArg: number) {
|
public universeRef: Universe;
|
||||||
|
|
||||||
|
constructor(universeArg: Universe, standardMessageExpiryArg: number) {
|
||||||
|
this.universeRef = universeArg;
|
||||||
this.standardMessageExpiry = standardMessageExpiryArg;
|
this.standardMessageExpiry = standardMessageExpiryArg;
|
||||||
|
this.blackListChannel = new UniverseChannel(this.universeRef, 'blacklist', 'nada');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,10 +54,13 @@ export class UniverseCache {
|
|||||||
* @param messageArg
|
* @param messageArg
|
||||||
* @param attachedPayloadArg
|
* @param attachedPayloadArg
|
||||||
*/
|
*/
|
||||||
public async addMessage(messageArg: UniverseMessage) {
|
public async addMessage(messageArg: UniverseMessage<any>) {
|
||||||
messageArg.setUniverseCache(this);
|
messageArg.setUniverseCache(this);
|
||||||
UniverseChannel.authorizeAMessageForAChannel(this, messageArg);
|
UniverseChannel.authorizeAMessageForAChannel(this, messageArg);
|
||||||
this.messageMap.add(messageArg);
|
this.messageMap.add(messageArg);
|
||||||
|
messageArg.universeChannelList.forEach(universeChannel => {
|
||||||
|
universeChannel.push(messageArg);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -55,7 +69,7 @@ export class UniverseCache {
|
|||||||
public readMessagesYoungerThan(
|
public readMessagesYoungerThan(
|
||||||
unixTimeArg?: number,
|
unixTimeArg?: number,
|
||||||
channelName?: string
|
channelName?: string
|
||||||
): Observable<UniverseMessage> {
|
): Observable<UniverseMessage<any>> {
|
||||||
const messageObservable = from(this.messageMap.getArray()).pipe(
|
const messageObservable = from(this.messageMap.getArray()).pipe(
|
||||||
filter(messageArg => {
|
filter(messageArg => {
|
||||||
return messageArg.smartTimestamp.isYoungerThanMilliSeconds(this.destructionTime);
|
return messageArg.smartTimestamp.isYoungerThanMilliSeconds(this.destructionTime);
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import * as plugins from './smartuniverse.plugins';
|
import * as plugins from './smartuniverse.plugins';
|
||||||
|
import * as interfaces from './interfaces';
|
||||||
|
|
||||||
import { Objectmap } from '@pushrocks/lik';
|
|
||||||
import { UniverseCache } from './smartuniverse.classes.universecache';
|
import { UniverseCache } from './smartuniverse.classes.universecache';
|
||||||
import { UniverseMessage } from './smartuniverse.classes.universemessage';
|
import { UniverseMessage } from './smartuniverse.classes.universemessage';
|
||||||
|
import { UniverseConnection } from './smartuniverse.classes.universeconnection';
|
||||||
|
import { Universe } from './smartuniverse.classes.universe';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* enables messages to stay within a certain scope.
|
* enables messages to stay within a certain scope.
|
||||||
@ -18,12 +20,12 @@ export class UniverseChannel {
|
|||||||
* @param passphraseArg the secret thats used for a certain topic.
|
* @param passphraseArg the secret thats used for a certain topic.
|
||||||
*/
|
*/
|
||||||
public static createChannel(
|
public static createChannel(
|
||||||
universeCacheArg: UniverseCache,
|
universeArg: Universe,
|
||||||
channelNameArg: string,
|
channelNameArg: string,
|
||||||
passphraseArg: string
|
passphraseArg: string
|
||||||
) {
|
) {
|
||||||
const newChannel = new UniverseChannel(universeCacheArg, channelNameArg, passphraseArg);
|
const newChannel = new UniverseChannel(universeArg, channelNameArg, passphraseArg);
|
||||||
universeCacheArg.channelMap.add(newChannel);
|
universeArg.universeCache.channelMap.add(newChannel);
|
||||||
return newChannel;
|
return newChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,10 +43,17 @@ export class UniverseChannel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a static message authorization function that takes the UniverseCache
|
||||||
|
* (where messages and channels are stored and their lifetime is managed)
|
||||||
|
* and the universemessage to find a fitting channel for the message
|
||||||
|
* @param universeCacheArg
|
||||||
|
* @param universeMessageArg
|
||||||
|
*/
|
||||||
public static authorizeAMessageForAChannel(
|
public static authorizeAMessageForAChannel(
|
||||||
universeCacheArg: UniverseCache,
|
universeCacheArg: UniverseCache,
|
||||||
universeMessageArg: UniverseMessage
|
universeMessageArg: UniverseMessage<any>
|
||||||
) {
|
): UniverseChannel {
|
||||||
const foundChannel = universeCacheArg.channelMap.find(universeChannel => {
|
const foundChannel = universeCacheArg.channelMap.find(universeChannel => {
|
||||||
const result = universeChannel.authenticate(universeMessageArg);
|
const result = universeChannel.authenticate(universeMessageArg);
|
||||||
return result;
|
return result;
|
||||||
@ -52,13 +61,22 @@ export class UniverseChannel {
|
|||||||
if (foundChannel) {
|
if (foundChannel) {
|
||||||
universeMessageArg.authenticated = true;
|
universeMessageArg.authenticated = true;
|
||||||
universeMessageArg.universeChannelList.add(foundChannel);
|
universeMessageArg.universeChannelList.add(foundChannel);
|
||||||
|
plugins.smartlog.defaultLogger.log('ok', 'message authorized');
|
||||||
return foundChannel;
|
return foundChannel;
|
||||||
} else {
|
} else {
|
||||||
universeMessageArg.authenticated = false;
|
universeMessageArg.authenticated = false;
|
||||||
universeMessageArg.universeChannelList.add(universeCacheArg.blackListChannel);
|
universeMessageArg.universeChannelList.add(universeCacheArg.blackListChannel);
|
||||||
|
plugins.smartlog.defaultLogger.log('warn', 'message not valid');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static getUniverseChannelByName(universeRef: Universe, universeChannelName: string) {
|
||||||
|
return universeRef.universeCache.channelMap.find(channelArg => {
|
||||||
|
return channelArg.name === universeChannelName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ========
|
// ========
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
// ========
|
// ========
|
||||||
@ -66,27 +84,85 @@ export class UniverseChannel {
|
|||||||
* the name of the channel
|
* the name of the channel
|
||||||
*/
|
*/
|
||||||
public name: string;
|
public name: string;
|
||||||
public universeCacheInstance: UniverseCache;
|
public universeRef: Universe;
|
||||||
|
private subject = new plugins.smartrx.rxjs.Subject<UniverseMessage<any>>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the passphrase for the channel
|
* the passphrase for the channel
|
||||||
*/
|
*/
|
||||||
public passphrase: string;
|
public passphrase: string;
|
||||||
|
|
||||||
constructor(universeCacheArg: UniverseCache, channelNameArg: string, passphraseArg: string) {
|
constructor(universeArg: Universe, channelNameArg: string, passphraseArg: string) {
|
||||||
|
this.universeRef = universeArg;
|
||||||
this.name = channelNameArg;
|
this.name = channelNameArg;
|
||||||
this.passphrase = passphraseArg;
|
this.passphrase = passphraseArg;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* authenticates a client on the server side
|
* authenticates a client on the server side by matching
|
||||||
|
* # the messages channelName against the unverseChannel's name
|
||||||
|
* # the messages password against the universeChannel's password
|
||||||
*/
|
*/
|
||||||
public authenticate(universeMessageArg: UniverseMessage): boolean {
|
public authenticate(universeMessageArg: UniverseMessage<any>): boolean {
|
||||||
return (
|
return (
|
||||||
this.name === universeMessageArg.targetChannelName &&
|
this.name === universeMessageArg.targetChannelName &&
|
||||||
this.passphrase === universeMessageArg.passphrase
|
this.passphrase === universeMessageArg.passphrase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public pushToClients(messageArg: UniverseMessage) {}
|
/**
|
||||||
|
* pushes a message to clients
|
||||||
|
* @param messageArg
|
||||||
|
*/
|
||||||
|
public async push(messageArg: UniverseMessage<any>) {
|
||||||
|
this.subject.next(messageArg);
|
||||||
|
const universeConnectionsWithChannelAccess: UniverseConnection[] = [];
|
||||||
|
await this.universeRef.universeCache.connectionMap.forEach(async socketConnection => {
|
||||||
|
if (socketConnection.authenticatedChannels.includes(this)) {
|
||||||
|
universeConnectionsWithChannelAccess.push(socketConnection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (const universeConnection of universeConnectionsWithChannelAccess) {
|
||||||
|
const smartsocket = universeConnection.socketConnection
|
||||||
|
.smartsocketRef as plugins.smartsocket.Smartsocket;
|
||||||
|
const universeMessageToSend: interfaces.IUniverseMessage = {
|
||||||
|
id: messageArg.id,
|
||||||
|
timestamp: messageArg.timestamp,
|
||||||
|
passphrase: messageArg.passphrase,
|
||||||
|
targetChannelName: this.name,
|
||||||
|
messageText: messageArg.messageText,
|
||||||
|
payload: messageArg.payload
|
||||||
|
};
|
||||||
|
smartsocket.clientCall(
|
||||||
|
'processMessage',
|
||||||
|
universeMessageToSend,
|
||||||
|
universeConnection.socketConnection
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// functions to interact with a channel locally
|
||||||
|
public subscribe(observingFunctionArg: (messageArg: UniverseMessage<any>) => void) {
|
||||||
|
return this.subject.subscribe(
|
||||||
|
messageArg => {
|
||||||
|
observingFunctionArg(messageArg);
|
||||||
|
},
|
||||||
|
error => console.log(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sends a message to the channel
|
||||||
|
*/
|
||||||
|
public async sendMessage(messageDescriptor: interfaces.IMessageCreator) {
|
||||||
|
const messageToSend = new UniverseMessage({
|
||||||
|
id: plugins.smartunique.shortId(),
|
||||||
|
messageText: messageDescriptor.messageText,
|
||||||
|
payload: messageDescriptor.payload,
|
||||||
|
targetChannelName: this.name,
|
||||||
|
passphrase: this.passphrase,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
this.universeRef.universeCache.addMessage(messageToSend);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,135 @@
|
|||||||
import * as plugins from './smartuniverse.plugins';
|
import * as plugins from './smartuniverse.plugins';
|
||||||
|
import * as interfaces from './interfaces';
|
||||||
import { UniverseChannel } from './smartuniverse.classes.universechannel';
|
import { UniverseChannel } from './smartuniverse.classes.universechannel';
|
||||||
|
import { UniverseCache } from './smartuniverse.classes.universecache';
|
||||||
|
import { Universe } from './smartuniverse.classes.universe';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* represents a connection to the universe
|
* represents a connection to the universe
|
||||||
*/
|
*/
|
||||||
export class UniverseConnection {
|
export class UniverseConnection {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param universeConnectionArg
|
||||||
|
*/
|
||||||
|
public static async addConnectionToCache(
|
||||||
|
universeRef: Universe,
|
||||||
|
universeConnectionArg: UniverseConnection
|
||||||
|
) {
|
||||||
|
let universeConnection = universeConnectionArg;
|
||||||
|
universeConnection = await UniverseConnection.deduplicateUniverseConnection(
|
||||||
|
universeRef.universeCache,
|
||||||
|
universeConnection
|
||||||
|
);
|
||||||
|
universeConnection = await UniverseConnection.authenticateAuthenticationRequests(
|
||||||
|
universeRef,
|
||||||
|
universeConnection
|
||||||
|
);
|
||||||
|
universeRef.universeCache.connectionMap.add(universeConnection);
|
||||||
|
console.log('hi');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* deduplicates UniverseConnections
|
||||||
|
*/
|
||||||
|
public static async deduplicateUniverseConnection(
|
||||||
|
universeCache: UniverseCache,
|
||||||
|
universeConnectionArg: UniverseConnection
|
||||||
|
): Promise<UniverseConnection> {
|
||||||
|
let connectionToReturn: UniverseConnection;
|
||||||
|
universeCache.connectionMap.forEach(async existingConnection => {
|
||||||
|
if (existingConnection.socketConnection === universeConnectionArg.socketConnection) {
|
||||||
|
connectionToReturn = await this.mergeUniverseConnections(
|
||||||
|
existingConnection,
|
||||||
|
universeConnectionArg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!connectionToReturn) {
|
||||||
|
connectionToReturn = universeConnectionArg;
|
||||||
|
}
|
||||||
|
return connectionToReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* authenticate AuthenticationRequests
|
||||||
|
*/
|
||||||
|
public static async authenticateAuthenticationRequests(
|
||||||
|
universeRef: Universe,
|
||||||
|
universeConnectionArg: UniverseConnection
|
||||||
|
): Promise<UniverseConnection> {
|
||||||
|
for (const authenticationRequest of universeConnectionArg.authenticationRequests) {
|
||||||
|
const universeChannelToAuthenticateAgainst = UniverseChannel.getUniverseChannelByName(
|
||||||
|
universeRef,
|
||||||
|
authenticationRequest.name
|
||||||
|
);
|
||||||
|
if (universeChannelToAuthenticateAgainst.passphrase === authenticationRequest.passphrase) {
|
||||||
|
universeConnectionArg.authenticatedChannels.push(universeChannelToAuthenticateAgainst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return universeConnectionArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* merges two UniverseConnections
|
||||||
|
*/
|
||||||
|
public static mergeUniverseConnections(
|
||||||
|
connectionArg1: UniverseConnection,
|
||||||
|
connectionArg2: UniverseConnection
|
||||||
|
) {
|
||||||
|
return connectionArg1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* finds a UniverseConnection by providing a socket connection
|
||||||
|
*/
|
||||||
|
public static findUniverseConnectionBySocketConnection(
|
||||||
|
universeCache: UniverseCache,
|
||||||
|
socketConnectionArg: plugins.smartsocket.SocketConnection
|
||||||
|
): UniverseConnection {
|
||||||
|
const universeConnection = universeCache.connectionMap.find(universeConnectionArg => {
|
||||||
|
return universeConnectionArg.socketConnection === socketConnectionArg;
|
||||||
|
});
|
||||||
|
return universeConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
public universeRef: Universe;
|
||||||
|
public terminatedDeferred = plugins.smartpromise.defer();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the socketClient to ping
|
* the socketClient to ping
|
||||||
*/
|
*/
|
||||||
public socketConnection: plugins.smartsocket.SocketConnection;
|
public socketConnection: plugins.smartsocket.SocketConnection;
|
||||||
public authenticationRequests = []
|
public authenticationRequests: Array<interfaces.ISocketRequest_SubscribeChannel['request']> = [];
|
||||||
public subscribedChannels: UniverseChannel[] = [];
|
|
||||||
public authenticatedChannels: UniverseChannel[] = [];
|
public authenticatedChannels: UniverseChannel[] = [];
|
||||||
public failedToJoinChannels: UniverseChannel[] = [];
|
public failedToJoinChannels: UniverseChannel[] = [];
|
||||||
|
|
||||||
public terminateConnection () {
|
/**
|
||||||
this.socketConnection
|
* disconnect the connection
|
||||||
|
*/
|
||||||
|
public async disconnect(reason: 'upstreamevent' | 'triggered' = 'triggered') {
|
||||||
|
if (reason === 'triggered') {
|
||||||
|
await this.socketConnection.disconnect();
|
||||||
|
}
|
||||||
|
this.universeRef.universeCache.connectionMap.remove(this);
|
||||||
|
this.terminatedDeferred.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(optionsArg: {
|
constructor(optionsArg: {
|
||||||
|
universe: Universe;
|
||||||
socketConnection: plugins.smartsocket.SocketConnection;
|
socketConnection: plugins.smartsocket.SocketConnection;
|
||||||
|
authenticationRequests: Array<interfaces.ISocketRequest_SubscribeChannel['request']>;
|
||||||
}) {
|
}) {
|
||||||
this.socketConnection,
|
this.universeRef = optionsArg.universe;
|
||||||
|
this.authenticationRequests = optionsArg.authenticationRequests;
|
||||||
|
this.socketConnection = optionsArg.socketConnection;
|
||||||
|
this.socketConnection.eventSubject.subscribe(async eventArg => {
|
||||||
|
switch (eventArg) {
|
||||||
|
case 'disconnected':
|
||||||
|
await this.disconnect('upstreamevent');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
import * as plugins from './smartuniverse.plugins';
|
|
||||||
import { UniverseConnection } from './smartuniverse.classes.universeconnection';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* manages connections to a universe
|
|
||||||
*/
|
|
||||||
export class UniverseConnectionManager {
|
|
||||||
public connectionMap = new plugins.lik.Objectmap<UniverseConnection>();
|
|
||||||
|
|
||||||
public addConnection() {}
|
|
||||||
}
|
|
@ -7,23 +7,30 @@ import { Timer, TimeStamp } from '@pushrocks/smarttime';
|
|||||||
import { Universe } from './smartuniverse.classes.universe';
|
import { Universe } from './smartuniverse.classes.universe';
|
||||||
import { UniverseChannel } from './smartuniverse.classes.universechannel';
|
import { UniverseChannel } from './smartuniverse.classes.universechannel';
|
||||||
import { UniverseCache } from './smartuniverse.classes.universecache';
|
import { UniverseCache } from './smartuniverse.classes.universecache';
|
||||||
import { IUniverseMessage } from './interfaces';
|
import { SocketConnection } from '@pushrocks/smartsocket';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* represents a message within a universe
|
* represents a message within a universe
|
||||||
* acts as a container to save message states like authentication status
|
* acts as a container to save message states like authentication status
|
||||||
*/
|
*/
|
||||||
export class UniverseMessage implements interfaces.IUniverseMessage {
|
export class UniverseMessage<T> implements interfaces.IUniverseMessage {
|
||||||
public id: string;
|
public static createMessageFromPayload(
|
||||||
|
socketConnectionArg: SocketConnection,
|
||||||
|
dataArg: interfaces.IUniverseMessage
|
||||||
|
) {
|
||||||
|
const universeMessageInstance = new UniverseMessage(dataArg);
|
||||||
|
universeMessageInstance.socketConnection = socketConnectionArg;
|
||||||
|
return universeMessageInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public id: string;
|
||||||
public timestamp: number;
|
public timestamp: number;
|
||||||
public smartTimestamp: TimeStamp;
|
public smartTimestamp: TimeStamp;
|
||||||
|
|
||||||
public messageText: string;
|
public messageText: string;
|
||||||
public passphrase: string;
|
public passphrase: string;
|
||||||
public payload: any;
|
public payload: T;
|
||||||
public payloadStringType;
|
|
||||||
public targetChannelName: string;
|
public targetChannelName: string;
|
||||||
|
public socketConnection: SocketConnection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the UniverseCache the message is attached to
|
* the UniverseCache the message is attached to
|
||||||
@ -38,7 +45,7 @@ export class UniverseMessage implements interfaces.IUniverseMessage {
|
|||||||
/**
|
/**
|
||||||
* wether the message is authenticated
|
* wether the message is authenticated
|
||||||
*/
|
*/
|
||||||
public authenticated: boolean = null;
|
public authenticated: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a destruction timer for this message
|
* a destruction timer for this message
|
||||||
@ -50,31 +57,41 @@ export class UniverseMessage implements interfaces.IUniverseMessage {
|
|||||||
* @param messageArg
|
* @param messageArg
|
||||||
* @param attachedPayloadArg
|
* @param attachedPayloadArg
|
||||||
*/
|
*/
|
||||||
constructor(messageDescriptor: IUniverseMessage) {
|
constructor(messageDescriptor: interfaces.IUniverseMessage) {
|
||||||
this.smartTimestamp = new TimeStamp(this.timestamp);
|
this.smartTimestamp = new TimeStamp(this.timestamp);
|
||||||
this.messageText = messageDescriptor.messageText;
|
this.messageText = messageDescriptor.messageText;
|
||||||
this.targetChannelName = messageDescriptor.targetChannelName;
|
this.targetChannelName = messageDescriptor.targetChannelName;
|
||||||
this.passphrase = messageDescriptor.passphrase;
|
this.passphrase = messageDescriptor.passphrase;
|
||||||
this.payload = messageDescriptor.payload;
|
this.payload = messageDescriptor.payload;
|
||||||
// prevent memory issues
|
// prevent memory issues
|
||||||
this.fallBackDestruction();
|
this.setDestructionTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public setUniverseCache(universeCacheArg: UniverseCache) {
|
public setUniverseCache(universeCacheArg: UniverseCache) {
|
||||||
this.universeCache = universeCacheArg;
|
this.universeCache = universeCacheArg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setDestructionTimer(selfdestructAfterArg: number) {
|
public setTargetChannel() {}
|
||||||
|
|
||||||
|
public setDestructionTimer(selfdestructAfterArg?: number) {
|
||||||
if (selfdestructAfterArg) {
|
if (selfdestructAfterArg) {
|
||||||
this.destructionTimer = new Timer(selfdestructAfterArg);
|
this.destructionTimer = new Timer(selfdestructAfterArg);
|
||||||
this.destructionTimer.start();
|
this.destructionTimer.start();
|
||||||
|
|
||||||
// set up self destruction by removing this from the parent messageCache
|
// set up self destruction by removing this from the parent messageCache
|
||||||
this.destructionTimer.completed.then(async () => {
|
this.destructionTimer.completed
|
||||||
this.universeCache.messageMap.remove(this);
|
.then(async () => {
|
||||||
});
|
this.universeCache.messageMap.remove(this);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
console.log(this);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.fallBackDestruction();
|
plugins.smartdelay.delayFor(1000).then(() => {
|
||||||
|
if (!this.destructionTimer) {
|
||||||
|
this.setDestructionTimer(6000);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,17 +99,6 @@ export class UniverseMessage implements interfaces.IUniverseMessage {
|
|||||||
* handles bad messages for further analysis
|
* handles bad messages for further analysis
|
||||||
*/
|
*/
|
||||||
public handleAsBadMessage() {
|
public handleAsBadMessage() {
|
||||||
console.log('received a bad message');
|
plugins.smartlog.defaultLogger.log('warn', 'received a bad message');
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* prevents memory leaks if channels have no default
|
|
||||||
*/
|
|
||||||
private fallBackDestruction() {
|
|
||||||
plugins.smartdelay.delayFor(1000).then(() => {
|
|
||||||
if (!this.destructionTimer) {
|
|
||||||
this.setDestructionTimer(6000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,18 @@ import * as path from 'path';
|
|||||||
|
|
||||||
export { path };
|
export { path };
|
||||||
|
|
||||||
|
// apiglobal scope
|
||||||
|
import * as typedrequestInterfaces from '@apiglobal/typedrequest-interfaces';
|
||||||
|
|
||||||
|
export { typedrequestInterfaces };
|
||||||
|
|
||||||
// pushrocks scope
|
// pushrocks scope
|
||||||
import * as lik from '@pushrocks/lik';
|
import * as lik from '@pushrocks/lik';
|
||||||
import * as smarthash from '@pushrocks/smarthash';
|
import * as smarthash from '@pushrocks/smarthash';
|
||||||
import * as smartdelay from '@pushrocks/smartdelay';
|
import * as smartdelay from '@pushrocks/smartdelay';
|
||||||
import * as smartexpress from '@pushrocks/smartexpress';
|
import * as smartexpress from '@pushrocks/smartexpress';
|
||||||
import * as smartfile from '@pushrocks/smartfile';
|
import * as smartfile from '@pushrocks/smartfile';
|
||||||
|
import * as smartlog from '@pushrocks/smartlog';
|
||||||
import * as smartpromise from '@pushrocks/smartpromise';
|
import * as smartpromise from '@pushrocks/smartpromise';
|
||||||
import * as smartrequest from '@pushrocks/smartrequest';
|
import * as smartrequest from '@pushrocks/smartrequest';
|
||||||
import * as smartrx from '@pushrocks/smartrx';
|
import * as smartrx from '@pushrocks/smartrx';
|
||||||
@ -22,6 +28,7 @@ export {
|
|||||||
smartdelay,
|
smartdelay,
|
||||||
smartexpress,
|
smartexpress,
|
||||||
smartfile,
|
smartfile,
|
||||||
|
smartlog,
|
||||||
smartpromise,
|
smartpromise,
|
||||||
smartrx,
|
smartrx,
|
||||||
smartrequest,
|
smartrequest,
|
||||||
|
Reference in New Issue
Block a user