├── .gitignore ├── Blog ├── 02Clients-EveryMessage.png ├── 02Clients-FirstResponse.png ├── 10Clients-EveryMessage.png ├── 10Clients-FirstResponse.png ├── 50Clients-EveryMessage.png ├── 50Clients-FirstResponse.png ├── FullScreenSalesforce.png ├── LessonsLearned.md ├── RepoLink.png ├── RepoLink.snagproj └── Stats.xlsx ├── BlogSalesforce.code-workspace ├── Procfile ├── Readme.md ├── Salesforce ├── .forceignore ├── .gitignore ├── .prettierrc ├── .vscode │ └── tasks.json ├── @ELTOROIT │ └── scripts │ │ ├── AnonymousApex.txt │ │ ├── CreateOrg.sh │ │ ├── DeleteOrgs.sh │ │ └── functions.sh ├── config │ └── project-scratch-def.json ├── deploy │ └── main │ │ └── default │ │ ├── applications │ │ └── RealTimeDemo.app-meta.xml │ │ ├── classes │ │ ├── Publisher.cls │ │ └── Publisher.cls-meta.xml │ │ ├── contentassets │ │ ├── connectionpngrepocom.asset │ │ └── connectionpngrepocom.asset-meta.xml │ │ ├── cspTrustedSites │ │ ├── WSDemo_Heroku.cspTrustedSite-meta.xml │ │ └── WSDemo_Localhost.cspTrustedSite-meta.xml │ │ ├── flexipages │ │ ├── Web_Sockets_UtilityBar.flexipage-meta.xml │ │ └── rtDashboard.flexipage-meta.xml │ │ ├── lwc │ │ ├── .eslintrc.json │ │ ├── client │ │ │ ├── client.css │ │ │ ├── client.html │ │ │ ├── client.js │ │ │ └── client.js-meta.xml │ │ ├── dashboard │ │ │ ├── dashboard.html │ │ │ ├── dashboard.js │ │ │ └── dashboard.js-meta.xml │ │ ├── streaming │ │ │ ├── streaming.js │ │ │ └── streaming.js-meta.xml │ │ └── websocket │ │ │ ├── websocket.js │ │ │ └── websocket.js-meta.xml │ │ ├── objects │ │ └── Demo__e │ │ │ ├── Demo__e.object-meta.xml │ │ │ └── fields │ │ │ ├── fromId__c.field-meta.xml │ │ │ ├── message__c.field-meta.xml │ │ │ ├── private__c.field-meta.xml │ │ │ ├── toId__c.field-meta.xml │ │ │ └── type__c.field-meta.xml │ │ ├── permissionsets │ │ └── WSS.permissionset-meta.xml │ │ ├── platformEventChannelMembers │ │ ├── ChangeEvents_AccountChangeEvent.platformEventChannelMember-meta.xml │ │ └── ChangeEvents_ContactChangeEvent.platformEventChannelMember-meta.xml │ │ └── tabs │ │ └── rtDashboard.tab-meta.xml ├── doNotDeploy │ └── main │ │ └── default │ │ └── lwc │ │ └── .eslintrc.json ├── package-lock.json ├── package.json └── sfdx-project.json ├── WebServer ├── .gitignore ├── .prettierrc ├── .vscode │ ├── launch.json │ └── tasks.json ├── Procfile ├── app │ ├── ETDataTypes.ts │ ├── ETRedis.ts │ ├── ETWebsocket.ts │ ├── ETWhereRUF.ts │ └── app.ts ├── cert.pem ├── key.pem ├── package.json ├── public │ ├── THLogo.png │ └── data.json ├── tsconfig.json └── views │ ├── pages │ ├── home.ejs │ └── ws.ejs │ └── partials │ └── header.ejs └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /Blog/02Clients-EveryMessage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eltoroit/ETWSBlogSalesforce/92ebb5097a9d3cab3f4b4fe3f0e539845bff3b80/Blog/02Clients-EveryMessage.png -------------------------------------------------------------------------------- /Blog/02Clients-FirstResponse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eltoroit/ETWSBlogSalesforce/92ebb5097a9d3cab3f4b4fe3f0e539845bff3b80/Blog/02Clients-FirstResponse.png -------------------------------------------------------------------------------- /Blog/10Clients-EveryMessage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eltoroit/ETWSBlogSalesforce/92ebb5097a9d3cab3f4b4fe3f0e539845bff3b80/Blog/10Clients-EveryMessage.png -------------------------------------------------------------------------------- /Blog/10Clients-FirstResponse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eltoroit/ETWSBlogSalesforce/92ebb5097a9d3cab3f4b4fe3f0e539845bff3b80/Blog/10Clients-FirstResponse.png -------------------------------------------------------------------------------- /Blog/50Clients-EveryMessage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eltoroit/ETWSBlogSalesforce/92ebb5097a9d3cab3f4b4fe3f0e539845bff3b80/Blog/50Clients-EveryMessage.png -------------------------------------------------------------------------------- /Blog/50Clients-FirstResponse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eltoroit/ETWSBlogSalesforce/92ebb5097a9d3cab3f4b4fe3f0e539845bff3b80/Blog/50Clients-FirstResponse.png -------------------------------------------------------------------------------- /Blog/FullScreenSalesforce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eltoroit/ETWSBlogSalesforce/92ebb5097a9d3cab3f4b4fe3f0e539845bff3b80/Blog/FullScreenSalesforce.png -------------------------------------------------------------------------------- /Blog/LessonsLearned.md: -------------------------------------------------------------------------------- 1 | # Lessons Learned 2 | 3 | - There is a delay for applying the CSP rules. It's not immediatly after the push! 4 | - Service components requires the metadata file even if the component does not use LWC (does not extend nor import) 5 | - Children components can be the rows in a table, if the CSS if applied `:host {display: contents;}` 6 | - How to organize the code now that @track is not required. 7 | - How to have private/untracked variables. 8 | - Redis 9 | - Two use cases 10 | - Heroku inter-dyno storage 11 | - Inter-dyno communication 12 | - Can be queried via Command prompt 13 | - Event emitter for events in Node.js 14 | -------------------------------------------------------------------------------- /Blog/RepoLink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eltoroit/ETWSBlogSalesforce/92ebb5097a9d3cab3f4b4fe3f0e539845bff3b80/Blog/RepoLink.png -------------------------------------------------------------------------------- /Blog/RepoLink.snagproj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eltoroit/ETWSBlogSalesforce/92ebb5097a9d3cab3f4b4fe3f0e539845bff3b80/Blog/RepoLink.snagproj -------------------------------------------------------------------------------- /Blog/Stats.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eltoroit/ETWSBlogSalesforce/92ebb5097a9d3cab3f4b4fe3f0e539845bff3b80/Blog/Stats.xlsx -------------------------------------------------------------------------------- /BlogSalesforce.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "Salesforce" 5 | }, 6 | { 7 | "path": "WebServer" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run serve --prefix WebServer -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # (Near) Real-Time Salesforce Applications 2 | 3 | ## What are Real-time applications? 4 | 5 | Let’s suppose we are building an application where we know the data are continuously changing, and we want a client to process that information as soon as possible. There are several ways we could architect such a solution. We either ping the application frequently to see if it has data for us (this is a bad idea), we can use Streaming API, or we could use WebSockets. 6 | 7 | ## So, What Are WebSockets? 8 | 9 | Websocket is a mechanism to enable real-time communication between a client and a server. 10 | 11 | ## Don’t we already have a Streaming API for this? 12 | 13 | Streaming API is a great mechanism for building these applications, and depending on your needs, you could use any of the 4 types of events: Pushtopics, Generic Streaming, Platform Events, and Change Data Capture. The difference is that with WebSockets, the client or the server can publish these messages. 14 | 15 | ## But Streaming API also allows the client to publish messages to the channel, don’t they? 16 | 17 | Well, kind of. Pushtopics and Change Data Capture (CDC) messages are always published by Salesforce when there are changes to the Salesforce data, so you can’t publish to those channels. 18 | 19 | ## But what about Generic Events or Platform Events? 20 | 21 | You could publish a Platform Events message from a Process Builder, a Flow, Apex code, REST API, or SOAP API. Salesforce puts the message on the channel. But you can’t publish directly on the channel itself! 22 | 23 | ## Those are just technical details; we are just splitting hairs. 24 | 25 | Not quite. Those are important details, and this makes WebSockets much faster. 26 | 27 | ## Are the WebSockets faster? 28 | 29 | Yes, they are! They are instantaneous, unlike Streaming API, where you can see a bit of a delay. And it’s not only because you publish directly on the channel, but also because they do not use Long Polling! 30 | 31 | ## What is Long Polling? 32 | 33 | Long Polling is the technique that Salesforce uses for Streaming API; it’s an implementation of the Bayeux protocol. The idea is interesting; it keeps the HTTPs connection open as long as possible, which is defined by the standards as 2 minutes. 34 | 35 | These are the steps for Streaming API: 36 | 37 | 1. The client makes an HTTPs call to Salesforce. 38 | 2. Salesforce receives the request but does not return an answer until it has one ready, or the request is about to expire (110 seconds). 39 | 3. The client receives the response and processes the data if there was any. Remember that it’s possible to have a response without data if the response returned because it would time out. 40 | 4. Repeat steps 1-3 forever! 41 | 42 | ## So how do WebSockets work? 43 | 44 | Great question! Let’s follow the wire just like we did for Long Polling. These are the steps for WebSockets: 45 | 46 | 1. The client makes an HTTPs call to Salesforce. 47 | 2. The server and the client flip the HTTPs connection into a WSS channel 48 | 3. The client or the server publish messages on this WSS channel 49 | 4. Repeat step 3. 50 | 51 | ## How is this different? Other than being more complicated because the connection has to switch from HTTPs to WSS. I do not get it! 52 | 53 | I understand your concern. With Long Polling, the client continuously repeats the 3 steps (1-3) because the connections do get closed. WebSockets take a tiny bit longer to load the application at the start. But after establishing the connection, it is kept open, and the client or the server publishes messages on that channel (only step 3). The connection remains open forever, and you do not need to keep re-open it. 54 | 55 | There is also a bit of a delay when publishing the event with Long Polling because it can not use the existing channel! 56 | 57 | ## I think that I get the idea, but is this noticeable? The communication between the client and the server is fast, right? 58 | 59 | I built a sample app to compare WebSockets vs. Platform Events, and I was surprised when I saw the numbers. 60 | 61 |

62 | 63 |

64 | 65 | This sample app allows me to create any number of clients and publish to all of them; then, I can see how long it took for every client to receive the messages. This chart shows the data when I run the test 100 times with 50 clients. As shown in the chart below, it took an average of 5 seconds for Platform Events, but it took about 0.2 seconds for WebSockets. 66 | 67 |

68 | 69 |

70 | 71 | I had seen the WebSocket clients were performing faster, but I was surprised when I saw these numbers. So I decided this was not a good test, because you would never have so many clients in a single browser (unless you are making a test application like this one). So I decided to answer a different question: How long does it take for the first message to arrive? This chart answers that question: 72 | 73 |

74 | 75 |

76 | 77 | We can see that WebSockets are still faster (0.050 seconds) than Platform Events (0.300 seconds) by a factor of 6X. 78 | 79 | Did you notice how predictable the WebSockets are? That was something that impressed me and noticed while I was running the tests for this blog. 80 | 81 | You can see the full details of the tests here. 82 | 83 | ## Wow. I am sold! Does Salesforce support having these WebSockets connections open forever? 84 | 85 | Well, yes and no! Salesforce servers do not support WebSockets. What y... 86 | 87 | ## STOP! Now I am baffled! 88 | 89 | What happened? 90 | 91 | ## WebSockets are cool, but I can’t use them. Game over! 92 | 93 | You did not let me finish the last answer. Hold on. Did you see the pictures I showed you before? Did you notice I was running WebSockets on Lightning Experience? 94 | 95 | ## Now you are just contradicting yourself! 96 | 97 | Let me explain, Salesforce servers do not allow you to use WebSockets, but the front-end does! The picture above is from an application I developed some components with LWC, and the JavaScript connects to the WSS server. 98 | 99 | This front-end could be built with Aura, LWC or Visualforce if you are still in classic (are you? Never mind!). Salesforce does allow you to make connections from JavaScript to a WebSocket server if you set the CSP. 100 | 101 | ## CSP? 102 | 103 | As explained in the documentation: “The Lightning Component framework uses Content Security Policy (CSP) to impose restrictions on content. The main objective is to help prevent cross-site scripting (XSS) and other code injection attacks. To use third-party APIs that make requests to an external (non-Salesforce) server or to use a WebSocket connection, add a CSP Trusted Site”. By the way, you are allowed to use WebSockets in CSP since Spring ‘19. 104 | 105 | ## If Salesforce is not the server, which server are you using? 106 | 107 | I build a NodeJs server that I run in localhost for testing and Heroku for production. You can see all the code in this repo. 108 | 109 |

110 | 111 | 112 | 113 |

114 | -------------------------------------------------------------------------------- /Salesforce/.forceignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status 2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm 3 | # 4 | 5 | package.xml 6 | 7 | # LWC configuration files 8 | **/jsconfig.json 9 | **/.eslintrc.json 10 | 11 | # LWC Jest 12 | **/__tests__/** 13 | 14 | # Ignore this... You should be working with Permission Sets 15 | **/profiles/** -------------------------------------------------------------------------------- /Salesforce/.gitignore: -------------------------------------------------------------------------------- 1 | .sfdx 2 | .DS_Store 3 | jsconfig.json 4 | .vscode/settings.json 5 | node_modules -------------------------------------------------------------------------------- /Salesforce/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "useTabs": true, 4 | "tabWidth": 4, 5 | "printWidth": 150, 6 | "overrides": [ 7 | { 8 | "files": "**/lwc/**/*.html", 9 | "options": { "parser": "lwc" } 10 | }, 11 | { 12 | "files": "*.{cmp,page,component}", 13 | "options": { "parser": "html" } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /Salesforce/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [{ 6 | "label": "ELTOROIT PushWhenBuild", 7 | "type": "shell", 8 | "command": "sfdx force:source:push --json", 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | }, 13 | "presentation": { 14 | "reveal": "always", 15 | "panel": "dedicated", 16 | "clear": true, 17 | "focus": false, 18 | "showReuseMessage": true 19 | } 20 | }] 21 | } 22 | -------------------------------------------------------------------------------- /Salesforce/@ELTOROIT/scripts/AnonymousApex.txt: -------------------------------------------------------------------------------- 1 | // Enable debug mode for the default scratch org user 2 | User defaultUser = [SELECT Id FROM User WHERE Id = :UserInfo.getUserId()]; 3 | defaultUser.UserPreferencesUserDebugModePref = true; 4 | update defaultUser; -------------------------------------------------------------------------------- /Salesforce/@ELTOROIT/scripts/CreateOrg.sh: -------------------------------------------------------------------------------- 1 | # Execute in Mac using: ./@ELTOROIT/scripts/CreateOrg.sh 2 | 3 | # --- Include helper scripts 4 | DIR="${BASH_SOURCE%/*}" 5 | if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi 6 | source "$DIR/functions.sh" 7 | 8 | # --- Batch variables 9 | # Alias for scratch org 10 | ALIAS="soWS2" 11 | 12 | # How long will the scratch org live (max 30) 13 | DAYS=1 14 | 15 | # Permission Set name 16 | PERM_SET=WSS 17 | 18 | # Path to Apex code to execute anonymously 19 | # Sample: "$DIR/AnonymousApex.txt" 20 | EXEC_ANON_APEX="$DIR/AnonymousApex.txt" 21 | 22 | # Is there any additional manual configuration required BEFORE pushing metadata? 23 | # Sample: /lightning/setup/SalesforceMobileAppQuickStart/home 24 | PATH2SETUP_METADATA_BEFORE= 25 | 26 | # Is there any additional manual configuration required AFTER pushing metadata? 27 | # Sample: /lightning/setup/SalesforceMobileAppQuickStart/home 28 | PATH2SETUP_METADATA_AFTER= 29 | 30 | # --- Batch boolean variables 31 | # Stop to validate org was succesfully created? Sometimes Sslesforce fails when creating an org and shows the login screen rather than opening an org. 32 | PAUSE2CHECK_ORG=true 33 | 34 | # Do you want to use ETCopyData to import data? 35 | IMPORT_DATA=false 36 | 37 | # Do you want to run Apex tests in this new org before starting? 38 | RUN_APEX_TESTS=false 39 | 40 | # Do you need a password for the user name? You may need this depening on the use of the new scratch org 41 | GENERATE_PASSWORD=true 42 | 43 | # --- Ready, set, go! 44 | everything -------------------------------------------------------------------------------- /Salesforce/@ELTOROIT/scripts/DeleteOrgs.sh: -------------------------------------------------------------------------------- 1 | echo "Looking for orgs to delete..." 2 | for row in $(sfdx force:org:list --json | jq -r '.result.scratchOrgs[] | select(. | has("alias") | not) | @base64'); do 3 | _jq() { 4 | echo ${row} | base64 --decode | jq -r ${1} 5 | } 6 | 7 | echo "Delete Org: $(_jq '.username')" 8 | sfdx force:org:delete --noprompt --targetusername $(_jq '.username') 9 | done 10 | sfdx force:org:list -------------------------------------------------------------------------------- /Salesforce/@ELTOROIT/scripts/functions.sh: -------------------------------------------------------------------------------- 1 | # Colors: 2 | # https://www.shellhacks.com/bash-colors/ 3 | # https://misc.flogisoft.com/bash/tip_colors_and_formatting 4 | 5 | function showStatus() { 6 | # Magenta 7 | echo "\033[0;35m$1\033[0m" 8 | } 9 | function showComplete() { 10 | # Green 11 | echo "\033[0;32mOperation Completed\033[0m" 12 | } 13 | function showPause(){ 14 | # Red 15 | echo "\033[0;31m" 16 | echo $1 17 | read -p "Press [Enter] key to continue... " 18 | echo "\033[0m" 19 | } 20 | 21 | function QuitError() { 22 | echo "\033[0;31m" 23 | echo "Org could not be created!" 24 | read -p "Press [Enter] key to continue... " 25 | echo "\033[0m" 26 | exit 1 27 | } 28 | 29 | function QuitSuccess() { 30 | # Green 31 | echo "\033[0;32m"; 32 | echo "*** *** *** *** *** *** *** *** *** ***" 33 | echo "*** *** Org created succesfully *** ***" 34 | echo "*** *** *** *** *** *** *** *** *** ***" 35 | echo "\033[0m" 36 | exit 0 37 | } 38 | 39 | function et_sfdx(){ 40 | echo "\033[2;30msfdx $*\033[0m" 41 | sfdx $* || QuitError 42 | } 43 | 44 | function jq_sfdx(){ 45 | echo "\033[2;30msfdx $*\033[0m" 46 | local sfdxResult=`sfdx $* || QuitError` 47 | } 48 | 49 | function everything() { 50 | # --- 51 | showStatus "*** Creating scratch Org..." 52 | et_sfdx force:org:create -f config/project-scratch-def.json --setdefaultusername --setalias "$ALIAS" -d "$DAYS" 53 | showComplete 54 | 55 | # --- 56 | if [[ "$PAUSE2CHECK_ORG" = true ]]; then 57 | showStatus "*** Opening scratch Org..." 58 | et_sfdx force:org:open 59 | showPause "Stop to validate the ORG was created succesfully." 60 | fi 61 | 62 | # --- 63 | if [[ ! -z "$PATH2SETUP_METADATA_BEFORE" ]]; then 64 | et_sfdx force:org:open --path "$PATH2SETUP_METADATA_BEFORE" 65 | showPause "Configure additonal metadata BEFORE pushing" 66 | fi 67 | 68 | # --- 69 | showStatus "*** Pushing metadata to scratch Org..." 70 | et_sfdx force:source:push --json 71 | showComplete 72 | 73 | if [[ ! -z "$PATH2SETUP_METADATA_AFTER" ]]; then 74 | et_sfdx force:org:open --path "$PATH2SETUP_METADATA_AFTER" 75 | showPause "Configure additonal metadata AFTER pushing" 76 | fi 77 | 78 | # --- 79 | if [ ! -z "$PERM_SET" ] 80 | then 81 | showStatus "*** Assigning permission set to your user..." 82 | et_sfdx force:user:permset:assign --permsetname "$PERM_SET" --json 83 | showComplete 84 | fi 85 | 86 | # --- 87 | if [ ! -z "$EXEC_ANON_APEX" ]; then 88 | showStatus "*** Execute Anonymous Apex..." 89 | et_sfdx force:apex:execute -f "$EXEC_ANON_APEX" 90 | showComplete 91 | fi 92 | 93 | # --- 94 | if [[ "$IMPORT_DATA" = true ]]; then 95 | showStatus "*** Creating data using ETCopyData plugin" 96 | # et_sfdx ETCopyData:export -c "./@ELTOROIT/data" --loglevel warn --json 97 | et_sfdx ETCopyData:import -c "./@ELTOROIT/data" --loglevel warn --json 98 | showComplete 99 | fi 100 | 101 | # --- 102 | if [[ "$RUN_APEX_TESTS" = true ]]; then 103 | showStatus "Runing Apex tests" 104 | jq_sfdx force:apex:test:run --codecoverage --synchronous --verbose --json --resultformat json 105 | echo $sfdxResult | jq "del(.result.tests, .result.coverage)" 106 | showComplete 107 | fi 108 | 109 | # --- 110 | if [[ "$GENERATE_PASSWORD" = true ]]; then 111 | showStatus "*** Generate Password..." 112 | et_sfdx force:user:password:generate --json 113 | et_sfdx force:user:display --json 114 | showComplete 115 | fi 116 | 117 | QuitSuccess 118 | } -------------------------------------------------------------------------------- /Salesforce/config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "ELTOROIT Demos Computing", 3 | "edition": "Developer", 4 | "hasSampleData": false, 5 | "settings": { 6 | "mobileSettings": { 7 | "enableS1EncryptedStoragePref2": false 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/applications/RealTimeDemo.app-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #0070D2 5 | connectionpngrepocom 6 | 1 7 | false 8 | 9 | Demo compares WebSockets with Platform events 10 | Small 11 | Large 12 | false 13 | false 14 | 15 | Standard 16 | rtDashboard 17 | Lightning 18 | Web_Sockets_UtilityBar 19 | 20 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/classes/Publisher.cls: -------------------------------------------------------------------------------- 1 | public class Publisher { 2 | @AuraEnabled 3 | public static void publish(String channel, String type, Integer fromId, Integer toId, String msg, Integer numberClients) { 4 | if (channel == '/event/Demo__e') { 5 | List demoEvents = new List(); 6 | if (toId == -1) { 7 | for (Integer i = 1; i <= numberClients; i++) { 8 | demoEvents.add(createEvent(channel, type, fromId, i, msg, false)); 9 | } 10 | } else { 11 | demoEvents.add(createEvent(channel, type, fromId, toId, msg, true)); 12 | } 13 | List results = EventBus.publish(demoEvents); 14 | // Inspect publishing result for each event 15 | for (Database.SaveResult sr : results) { 16 | if (sr.isSuccess()) { 17 | System.debug('Successfully published event.'); 18 | } else { 19 | for (Database.Error err : sr.getErrors()) { 20 | System.debug('Error returned: ' + err.getStatusCode() + ' - ' + err.getMessage()); 21 | } 22 | } 23 | } 24 | } 25 | } 26 | 27 | private static Demo__e createEvent(String channel, String type, Integer fromId, Integer toId, String msg, Boolean isPrivate) { 28 | Demo__e d = new Demo__e(); 29 | d.type__c = type; 30 | d.fromId__c = fromId; 31 | d.toId__c = toId; 32 | d.message__c = msg; 33 | d.private__c = isPrivate; 34 | System.debug(d); 35 | return d; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/classes/Publisher.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/contentassets/connectionpngrepocom.asset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eltoroit/ETWSBlogSalesforce/92ebb5097a9d3cab3f4b4fe3f0e539845bff3b80/Salesforce/deploy/main/default/contentassets/connectionpngrepocom.asset -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/contentassets/connectionpngrepocom.asset-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | en_US 5 | connectionpngrepocom 6 | 7 | 8 | VIEWER 9 | 10 | 11 | 12 | 13 | 1 14 | connection-pngrepo-com.png 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/cspTrustedSites/WSDemo_Heroku.cspTrustedSite-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | All 4 | wss://test-wsock01.herokuapp.com/ws?wsId=1 5 | wss://test-wsock01.herokuapp.com 6 | true 7 | 8 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/cspTrustedSites/WSDemo_Localhost.cspTrustedSite-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | All 4 | wss://localhost:5001/ws?fromWsId=test1 5 | wss://localhost:5001 6 | true 7 | 8 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/flexipages/Web_Sockets_UtilityBar.flexipage-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | utilityItems 5 | Region 6 | 7 | 8 | backgroundComponents 9 | Background 10 | 11 | Web Sockets UtilityBar 12 | 15 | UtilityBar 16 | 17 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/flexipages/rtDashboard.flexipage-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | dashboard 6 | 7 | main 8 | Region 9 | 10 | Dashboard 11 | 14 | AppPage 15 | 16 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/lwc/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@salesforce/eslint-config-lwc/recommended"] 3 | } 4 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/lwc/client/client.css: -------------------------------------------------------------------------------- 1 | :host { 2 | /* This allows the component to bt the actual rows for the table defined in the parent component */ 3 | display: contents; 4 | } 5 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/lwc/client/client.html: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/lwc/client/client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable no-alert */ 3 | /* eslint-disable no-console */ 4 | import { LightningElement, api } from "lwc"; 5 | import Websocket from "c/websocket"; 6 | import Streaming from "c/streaming"; 7 | 8 | export default class Client extends LightningElement { 9 | private = { 10 | client: null, 11 | channel: null, 12 | messages: [], 13 | numberClients: null 14 | }; 15 | 16 | msg = ""; 17 | toId = -1; 18 | title = ""; 19 | received = ""; 20 | isConnected = false; 21 | 22 | @api clientId; 23 | 24 | @api 25 | get channel() { 26 | return this.private.channel; 27 | } 28 | set channel(value) { 29 | if (this.private.channel !== value) { 30 | // Changed 31 | this.private.channel = value; 32 | this._initializeClient(); 33 | } 34 | } 35 | 36 | @api 37 | get numberClients() { 38 | return this.private.numberClients; 39 | } 40 | set numberClients(value) { 41 | this.private.numberClients = value; 42 | this._initializeClient(); 43 | } 44 | 45 | @api 46 | open() { 47 | this.private.client.open(); 48 | } 49 | 50 | @api 51 | close() { 52 | this._resetClient(); 53 | this.private.client.close(); 54 | } 55 | 56 | connectedCallback() { 57 | this._initializeClient(); 58 | } 59 | 60 | handleMessageChange(event) { 61 | this.msg = event.target.value; 62 | } 63 | 64 | handleToIdChange(event) { 65 | this.toId = event.target.value; 66 | } 67 | 68 | handlePublishClick() { 69 | this.dispatchEvent(new CustomEvent("countposts", { detail: 0 })); 70 | const msg = this.private.client.buildMessage("POST", this.toId, this.msg); 71 | this.private.client.publish(msg); 72 | } 73 | 74 | handleNotifyState(event) { 75 | let detail = event.detail; 76 | this.title = this.private.client.title; 77 | this.isConnected = this.private.client.isConnected; 78 | 79 | detail.clientId = this.clientId; 80 | this.dispatchEvent(new CustomEvent("notifystate", { detail })); 81 | } 82 | 83 | handleMessage(event) { 84 | const data = event.detail; 85 | this.title = this.private.client.title; 86 | 87 | this.received = `${data.type}: ${data.msg}
`; 88 | if (data.fromId !== data.toId) { 89 | this.received += `${data.fromId}@${data.fromDyno} => ${data.toId}@${data.toDyno}.
`; 90 | } 91 | this.received += `${data.private ? "Private" : "Public"} @ `; 92 | this.received += new Date(data.dttm).toLocaleDateString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" }); 93 | 94 | this.private.messages.unshift({ 95 | key: this.private.messages.length, 96 | data: data 97 | }); 98 | 99 | if (data.type === "POST") { 100 | this.dispatchEvent(new CustomEvent("countposts", { detail: +1 })); 101 | } 102 | } 103 | 104 | _initializeClient() { 105 | this._resetClient(); 106 | const isStreaming = this.private.channel.indexOf("wss") !== 0; 107 | if (isStreaming) { 108 | this.private.client = new Streaming(); 109 | this.private.client.numberClients = this.numberClients; 110 | } else { 111 | this.private.client = new Websocket(); 112 | } 113 | this.private.client.channel = this.channel; 114 | this.private.client.clientId = this.clientId; 115 | this.private.client.on("message", event => { 116 | this.handleMessage(event); 117 | }); 118 | this.private.client.on("notifystate", event => { 119 | this.handleNotifyState(event); 120 | }); 121 | } 122 | 123 | _resetClient() { 124 | this.msg = ""; 125 | this.toId = -1; 126 | this.title = ""; 127 | this.received = ""; 128 | this.isConnected = false; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/lwc/client/client.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | false 5 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/lwc/dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 | 73 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/lwc/dashboard/dashboard.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-debugger */ 2 | /* eslint-disable no-alert */ 3 | import { LightningElement } from "lwc"; 4 | 5 | export default class Dashboard extends LightningElement { 6 | channelUrl = null; 7 | channelData = { 8 | all: [ 9 | { label: "Localhost", value: "WSS|localhost:5001" }, 10 | { label: "Heroku", value: "WSS|test-wsock01.herokuapp.com" }, 11 | { label: "Platform Events", value: "PE|/event/Demo__e" } 12 | ], 13 | selected: null 14 | }; 15 | 16 | postCounter = null; 17 | postData = { 18 | firstTime: null, 19 | masterTimer: null, 20 | message: null 21 | }; 22 | 23 | clientDataRefresh = 0; 24 | clientData = { 25 | all: [], 26 | count: 5, 27 | stateValues: [new Set(), new Set(), new Set(), new Set()], 28 | stateNames: { CONNECTING: 0, OPEN: 1, CLOSING: 2, CLOSED: 3 } 29 | }; 30 | 31 | constructor() { 32 | super(); 33 | this.channel = this.channelData.all[1].value; 34 | } 35 | 36 | get channel() { 37 | return this.channelData.selected; 38 | } 39 | set channel(value) { 40 | let parts = value.split("|"); 41 | this.channelData.selected = value; 42 | switch (parts[0]) { 43 | case "PE": 44 | this.channelUrl = parts[1]; 45 | break; 46 | default: 47 | this.channelUrl = `wss://${parts[1]}/ws`; 48 | break; 49 | } 50 | this.createClients(); 51 | } 52 | 53 | get displayStates() { 54 | return Object.keys(this.clientData.stateNames).map(stateName => { 55 | let stateIdx = this.clientData.stateNames[stateName]; 56 | return { name: stateName, count: this.clientData.stateValues[stateIdx].size }; 57 | }); 58 | } 59 | 60 | connectedCallback() { 61 | this.createClients(); 62 | } 63 | 64 | handleChannelChange(event) { 65 | this.channel = event.detail.value; 66 | } 67 | 68 | handleNumberClientsChanged(event) { 69 | this.clientData.count = Number(event.target.value); 70 | this.createClients(); 71 | } 72 | 73 | handleNotifyState(event) { 74 | const data = event.detail; 75 | if (data.old) { 76 | this.clientData.stateValues[this.clientData.stateNames[data.old]].delete(data.clientId); 77 | } 78 | this.clientData.stateValues[this.clientData.stateNames[data.new]].add(data.clientId); 79 | this.clientDataRefresh++; 80 | } 81 | 82 | handleCountPosts(event) { 83 | const value = event.detail; 84 | if (value === 0) { 85 | this.postCounter = 0; 86 | this.postData.message = ""; 87 | this.postData.masterTimer = new Date(); 88 | } else { 89 | if (this.postCounter === null) { 90 | this.postCounter = 0; 91 | this.postData.message = ""; 92 | this.postData.masterTimer = new Date(); 93 | } 94 | this.postCounter++; 95 | if (this.postCounter === 1) { 96 | this.postData.firstTime = new Date() - this.postData.masterTimer; 97 | this.postData.message = ` (First: ${this.postData.firstTime / 1000.0} seconds)`; 98 | } else if (this.postCounter === this.clientData.count) { 99 | const diff = new Date() - this.postData.masterTimer; 100 | this.postData.message = ` (First: ${this.postData.firstTime / 1000.0}, Total: ${diff / 1000.0} seconds)`; 101 | } 102 | } 103 | } 104 | 105 | handleResetPosts() { 106 | this.postCounter = null; 107 | this.postData.message = ""; 108 | } 109 | 110 | openAll() { 111 | this.template.querySelectorAll("c-client").forEach(client => { 112 | client.open(); 113 | }); 114 | } 115 | 116 | closeAll() { 117 | this.template.querySelectorAll("c-client").forEach(client => { 118 | client.close(); 119 | }); 120 | } 121 | 122 | createClients() { 123 | if (this.clientData.all.length > 0) { 124 | this.closeAll(); 125 | } 126 | 127 | this.clientData.all = []; 128 | for (let i = 1; i <= this.clientData.count; i++) { 129 | this.clientData.all.push({ 130 | key: i 131 | }); 132 | this.clientData.stateValues[this.clientData.stateNames.CLOSED].add(i); 133 | } 134 | this.clientDataRefresh++; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/lwc/dashboard/dashboard.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | true 5 | 6 | lightning__AppPage 7 | 8 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/lwc/streaming/streaming.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert */ 2 | /* eslint-disable no-console */ 3 | import { subscribe, unsubscribe, onError } from "lightning/empApi"; 4 | import publisher from "@salesforce/apex/Publisher.publish"; 5 | 6 | export default class Streaming { 7 | _events = {}; 8 | _client = null; 9 | _oldState = "CLOSED"; 10 | _manualClose = false; 11 | _isConnected = false; 12 | _subscription = {}; 13 | 14 | channel; 15 | clientId; 16 | numberClients; 17 | 18 | get title() { 19 | return `${this.clientId}`; 20 | } 21 | 22 | get isConnected() { 23 | return this._isConnected; 24 | } 25 | 26 | open() { 27 | if (!this._isConnected) { 28 | this._client = null; 29 | this._manualClose = false; 30 | this._initialize(); 31 | } 32 | } 33 | 34 | close() { 35 | this._manualClose = true; 36 | if (this._isConnected) { 37 | this._close(); 38 | } 39 | } 40 | 41 | publish(message) { 42 | message.channel = this.channel; 43 | message.numberClients = this.numberClients; 44 | console.log(`PUBLISHING: ${JSON.stringify(message)}`); 45 | publisher(message) 46 | .then(result => { 47 | this._notifyState("OPEN", "publish", result); 48 | }) 49 | .catch(err => { 50 | alert(`Error: ${JSON.stringify(err)}`); 51 | }); 52 | } 53 | 54 | buildMessage(type, toId, data) { 55 | return { 56 | type, 57 | fromId: this.clientId, 58 | toId: toId, 59 | msg: data ? data : "NO DATA", 60 | private: "N/A", 61 | fromDyno: "N/A", 62 | toDyno: "N/A", 63 | dttm: new Date() 64 | }; 65 | } 66 | 67 | // Make this a service component 68 | on(eventName, callback) { 69 | this._events[eventName] = callback; 70 | } 71 | dispatchEvent(event) { 72 | if (this._events[event.type]) { 73 | this._events[event.type]({ detail: event.detail }); 74 | } 75 | } 76 | 77 | _initialize() { 78 | subscribe(this.channel, -1, response => { 79 | // console.log("New message received : ", JSON.stringify(response)); 80 | this._notifyState("OPEN", "message", response); 81 | const payload = response.data.payload; 82 | if (this.clientId === payload.toId__c) { 83 | this.dispatchEvent( 84 | new CustomEvent("message", { 85 | detail: { 86 | type: payload.type__c, 87 | fromId: payload.fromId__c, 88 | toId: payload.toId__c, 89 | msg: payload.message__c, 90 | private: payload.private__c, 91 | fromDyno: "N/A", 92 | toDyno: "N/A", 93 | dttm: new Date() 94 | } 95 | }) 96 | ); 97 | } 98 | }).then(response => { 99 | this._client = response; 100 | this._isConnected = true; 101 | console.log("Successfully subscribed to : ", JSON.stringify(response.channel)); 102 | this._notifyState("OPEN", "Subscribe", response); 103 | this.dispatchEvent(new CustomEvent("message", { detail: this.buildMessage("OPEN", this.clientId, "Subscribe") })); 104 | }); 105 | onError(error => { 106 | console.error("Received ERROR from server: ", JSON.stringify(error)); 107 | this._close(); 108 | }); 109 | } 110 | 111 | _close() { 112 | this._isConnected = false; 113 | unsubscribe(this._client, response => { 114 | console.log("unsubscribe() response: ", JSON.stringify(response)); 115 | }); 116 | this._notifyState("CLOSED", "Unsubscribe", {}); 117 | } 118 | 119 | _notifyState(newState, operation, event) { 120 | this.dispatchEvent(new CustomEvent("notifystate", { detail: { old: this._oldState, new: newState, operation, event } })); 121 | this._oldState = newState; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/lwc/streaming/streaming.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | false 5 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/lwc/websocket/websocket.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-debugger */ 2 | /* eslint-disable no-alert */ 3 | /* eslint-disable no-console */ 4 | 5 | export default class Websocket { 6 | _Id; 7 | _events = {}; 8 | _dyno = null; 9 | _pinger = null; 10 | _client = null; 11 | _oldState = "CLOSED"; 12 | _manualClose = false; 13 | _isConnected = false; 14 | 15 | channel; 16 | clientId; 17 | 18 | get title() { 19 | let title = ""; 20 | if (this._Id && this._dyno) { 21 | title = `${this._Id}@${this._dyno}`; 22 | } 23 | return title; 24 | } 25 | 26 | get isConnected() { 27 | return this._isConnected; 28 | } 29 | 30 | open() { 31 | if (!this._isConnected) { 32 | this._client = null; 33 | this._manualClose = false; 34 | this._initialize(); 35 | } 36 | } 37 | 38 | close() { 39 | this._manualClose = true; 40 | if (this._isConnected) { 41 | this._Id = null; 42 | this._client.close(); 43 | } 44 | } 45 | 46 | publish(message) { 47 | // Message is the full message, not just a string. You could use this.buildMessage() to build it. 48 | this._client.send(message); 49 | this._notifyState("publish", undefined); 50 | } 51 | 52 | buildMessage(type, toId, data) { 53 | return JSON.stringify({ type, fromWsId: this._Id, toWsId: toId, msg: data ? data : "NO DATA", dttm: new Date() }, null, 2); 54 | } 55 | 56 | // Make this a service component 57 | on(eventName, callback) { 58 | this._events[eventName] = callback; 59 | } 60 | dispatchEvent(event) { 61 | if (this._events[event.type]) { 62 | this._events[event.type]({ detail: event.detail }); 63 | } 64 | } 65 | 66 | _initialize() { 67 | if (this._client) { 68 | return; 69 | } 70 | 71 | if (this.channel) { 72 | this._client = new WebSocket(this.channel); 73 | this._notifyState("initialize", undefined); 74 | } else { 75 | this._client = null; 76 | alert(`server: [${this.channel}] has not been set!`); 77 | return; 78 | } 79 | 80 | this._client.onclose = event => { 81 | this._isConnected = false; 82 | this._notifyState("close", event); 83 | 84 | if (!this._manualClose) { 85 | // Try to reconnect. 86 | // event.code === 1000 means that the connection was closed normally. 87 | 88 | if (!navigator.onLine) { 89 | alert("You are offline. Please connect to the Internet and try again."); 90 | } else { 91 | this.open(); 92 | } 93 | } 94 | }; 95 | 96 | this._client.onerror = event => { 97 | this._Id = ""; 98 | this._isConnected = false; 99 | clearInterval(this._pinger); 100 | this._notifyState("error", event); 101 | }; 102 | 103 | this._client.onmessage = event => { 104 | const data = event.data; 105 | let jsonData = JSON.parse(data); 106 | this._notifyState("message", event); 107 | 108 | if (jsonData.type === "REGISTERED") { 109 | this._Id = jsonData.fromWsId; 110 | } 111 | 112 | if (["CONNECTED", "ECHO", "PONG"].includes(jsonData.type)) { 113 | const oldDyno = this._dyno; 114 | const newDyno = jsonData.fromDyno; 115 | this._dyno = newDyno; 116 | 117 | if (oldDyno && oldDyno !== newDyno) { 118 | const msg = JSON.stringify({ msg: "this._dyno changed", fromId: this._Id, oldDyno, newDyno }, null, 1); 119 | // alert(msg); 120 | console.log(msg); 121 | } 122 | } 123 | 124 | if (jsonData.type !== "PONG") { 125 | jsonData.toId = jsonData.toWsId; 126 | jsonData.fromId = jsonData.fromWsId; 127 | delete jsonData.toWsId; 128 | delete jsonData.fromWsId; 129 | this.dispatchEvent(new CustomEvent("message", { detail: jsonData })); 130 | } 131 | }; 132 | 133 | this._client.onopen = event => { 134 | this._isConnected = true; 135 | this._manualClose = false; 136 | this._notifyState("open", event); 137 | 138 | // eslint-disable-next-line @lwc/lwc/no-async-operation 139 | this._pinger = setInterval(() => { 140 | if (this._isConnected && this._Id) { 141 | this._client.send(this.buildMessage("PING", 0, "Keep Alive")); 142 | this._notifyState("timer", undefined); 143 | } 144 | }, 30 * 1000); 145 | }; 146 | } 147 | 148 | _notifyState(operation, event) { 149 | const newState = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"][this._client.readyState]; 150 | this.dispatchEvent(new CustomEvent("notifystate", { detail: { old: this._oldState, new: newState, operation, event } })); 151 | this._oldState = newState; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/lwc/websocket/websocket.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | false 5 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/objects/Demo__e/Demo__e.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Deployed 4 | HighVolume 5 | 6 | Demos 7 | PublishImmediately 8 | 9 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/objects/Demo__e/fields/fromId__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | fromId__c 4 | false 5 | false 6 | false 7 | false 8 | 9 | 3 10 | true 11 | 0 12 | Number 13 | false 14 | 15 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/objects/Demo__e/fields/message__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | message__c 4 | false 5 | false 6 | false 7 | false 8 | 9 | 255 10 | true 11 | Text 12 | false 13 | 14 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/objects/Demo__e/fields/private__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | private__c 4 | false 5 | false 6 | false 7 | false 8 | false 9 | 10 | Checkbox 11 | 12 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/objects/Demo__e/fields/toId__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | toId__c 4 | false 5 | false 6 | false 7 | false 8 | 9 | 3 10 | true 11 | 0 12 | Number 13 | false 14 | 15 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/objects/Demo__e/fields/type__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | type__c 4 | false 5 | false 6 | false 7 | false 8 | 9 | 50 10 | true 11 | Text 12 | false 13 | 14 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/permissionsets/WSS.permissionset-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RealTimeDemo 5 | true 6 | 7 | false 8 | 9 | 10 | true 11 | false 12 | false 13 | true 14 | false 15 | Demo__e 16 | false 17 | 18 | 19 | rtDashboard 20 | Visible 21 | 22 | 23 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/platformEventChannelMembers/ChangeEvents_AccountChangeEvent.platformEventChannelMember-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ChangeEvents 4 | AccountChangeEvent 5 | 6 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/platformEventChannelMembers/ChangeEvents_ContactChangeEvent.platformEventChannelMember-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ChangeEvents 4 | ContactChangeEvent 5 | 6 | -------------------------------------------------------------------------------- /Salesforce/deploy/main/default/tabs/rtDashboard.tab-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | rtDashboard 4 | 5 | Custom67: Gears 6 | 7 | -------------------------------------------------------------------------------- /Salesforce/doNotDeploy/main/default/lwc/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@salesforce/eslint-config-lwc/recommended" 3 | } -------------------------------------------------------------------------------- /Salesforce/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "etwsblogsalesforce_sf", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@hapi/address": { 8 | "version": "2.1.4", 9 | "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", 10 | "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==", 11 | "dev": true 12 | }, 13 | "@hapi/formula": { 14 | "version": "1.2.0", 15 | "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-1.2.0.tgz", 16 | "integrity": "sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA==", 17 | "dev": true 18 | }, 19 | "@hapi/hoek": { 20 | "version": "8.5.1", 21 | "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", 22 | "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==", 23 | "dev": true 24 | }, 25 | "@hapi/joi": { 26 | "version": "16.1.8", 27 | "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-16.1.8.tgz", 28 | "integrity": "sha512-wAsVvTPe+FwSrsAurNt5vkg3zo+TblvC5Bb1zMVK6SJzZqw9UrJnexxR+76cpePmtUZKHAPxcQ2Bf7oVHyahhg==", 29 | "dev": true, 30 | "requires": { 31 | "@hapi/address": "^2.1.2", 32 | "@hapi/formula": "^1.2.0", 33 | "@hapi/hoek": "^8.2.4", 34 | "@hapi/pinpoint": "^1.0.2", 35 | "@hapi/topo": "^3.1.3" 36 | } 37 | }, 38 | "@hapi/pinpoint": { 39 | "version": "1.0.2", 40 | "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-1.0.2.tgz", 41 | "integrity": "sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ==", 42 | "dev": true 43 | }, 44 | "@hapi/topo": { 45 | "version": "3.1.6", 46 | "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", 47 | "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", 48 | "dev": true, 49 | "requires": { 50 | "@hapi/hoek": "^8.3.0" 51 | } 52 | }, 53 | "@types/color-name": { 54 | "version": "1.1.1", 55 | "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", 56 | "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", 57 | "dev": true 58 | }, 59 | "ajv": { 60 | "version": "6.11.0", 61 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", 62 | "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", 63 | "dev": true, 64 | "requires": { 65 | "fast-deep-equal": "^3.1.1", 66 | "fast-json-stable-stringify": "^2.0.0", 67 | "json-schema-traverse": "^0.4.1", 68 | "uri-js": "^4.2.2" 69 | } 70 | }, 71 | "ansi-regex": { 72 | "version": "5.0.0", 73 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 74 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", 75 | "dev": true 76 | }, 77 | "ansi-styles": { 78 | "version": "4.2.1", 79 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", 80 | "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", 81 | "dev": true, 82 | "requires": { 83 | "@types/color-name": "^1.1.1", 84 | "color-convert": "^2.0.1" 85 | } 86 | }, 87 | "asn1": { 88 | "version": "0.2.4", 89 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 90 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 91 | "dev": true, 92 | "requires": { 93 | "safer-buffer": "~2.1.0" 94 | } 95 | }, 96 | "assert-plus": { 97 | "version": "1.0.0", 98 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 99 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", 100 | "dev": true 101 | }, 102 | "asynckit": { 103 | "version": "0.4.0", 104 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 105 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", 106 | "dev": true 107 | }, 108 | "aws-sign2": { 109 | "version": "0.7.0", 110 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 111 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", 112 | "dev": true 113 | }, 114 | "aws4": { 115 | "version": "1.9.1", 116 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", 117 | "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", 118 | "dev": true 119 | }, 120 | "axios": { 121 | "version": "0.19.2", 122 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", 123 | "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", 124 | "dev": true, 125 | "requires": { 126 | "follow-redirects": "1.5.10" 127 | } 128 | }, 129 | "bcrypt-pbkdf": { 130 | "version": "1.0.2", 131 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 132 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 133 | "dev": true, 134 | "requires": { 135 | "tweetnacl": "^0.14.3" 136 | } 137 | }, 138 | "camelcase": { 139 | "version": "5.3.1", 140 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", 141 | "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", 142 | "dev": true 143 | }, 144 | "caseless": { 145 | "version": "0.12.0", 146 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 147 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", 148 | "dev": true 149 | }, 150 | "cliui": { 151 | "version": "6.0.0", 152 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", 153 | "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", 154 | "dev": true, 155 | "requires": { 156 | "string-width": "^4.2.0", 157 | "strip-ansi": "^6.0.0", 158 | "wrap-ansi": "^6.2.0" 159 | } 160 | }, 161 | "color-convert": { 162 | "version": "2.0.1", 163 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 164 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 165 | "dev": true, 166 | "requires": { 167 | "color-name": "~1.1.4" 168 | } 169 | }, 170 | "color-name": { 171 | "version": "1.1.4", 172 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 173 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 174 | "dev": true 175 | }, 176 | "combined-stream": { 177 | "version": "1.0.8", 178 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 179 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 180 | "dev": true, 181 | "requires": { 182 | "delayed-stream": "~1.0.0" 183 | } 184 | }, 185 | "core-util-is": { 186 | "version": "1.0.2", 187 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 188 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 189 | "dev": true 190 | }, 191 | "dashdash": { 192 | "version": "1.14.1", 193 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 194 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 195 | "dev": true, 196 | "requires": { 197 | "assert-plus": "^1.0.0" 198 | } 199 | }, 200 | "debug": { 201 | "version": "3.1.0", 202 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 203 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 204 | "dev": true, 205 | "requires": { 206 | "ms": "2.0.0" 207 | } 208 | }, 209 | "decamelize": { 210 | "version": "1.2.0", 211 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 212 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", 213 | "dev": true 214 | }, 215 | "delayed-stream": { 216 | "version": "1.0.0", 217 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 218 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 219 | "dev": true 220 | }, 221 | "detect-newline": { 222 | "version": "3.1.0", 223 | "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", 224 | "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", 225 | "dev": true 226 | }, 227 | "ecc-jsbn": { 228 | "version": "0.1.2", 229 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 230 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 231 | "dev": true, 232 | "requires": { 233 | "jsbn": "~0.1.0", 234 | "safer-buffer": "^2.1.0" 235 | } 236 | }, 237 | "emoji-regex": { 238 | "version": "8.0.0", 239 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 240 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 241 | "dev": true 242 | }, 243 | "extend": { 244 | "version": "3.0.2", 245 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 246 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 247 | "dev": true 248 | }, 249 | "extsprintf": { 250 | "version": "1.3.0", 251 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 252 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", 253 | "dev": true 254 | }, 255 | "fast-deep-equal": { 256 | "version": "3.1.1", 257 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", 258 | "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", 259 | "dev": true 260 | }, 261 | "fast-json-stable-stringify": { 262 | "version": "2.1.0", 263 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 264 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 265 | "dev": true 266 | }, 267 | "find-up": { 268 | "version": "4.1.0", 269 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", 270 | "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", 271 | "dev": true, 272 | "requires": { 273 | "locate-path": "^5.0.0", 274 | "path-exists": "^4.0.0" 275 | } 276 | }, 277 | "follow-redirects": { 278 | "version": "1.5.10", 279 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", 280 | "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", 281 | "dev": true, 282 | "requires": { 283 | "debug": "=3.1.0" 284 | } 285 | }, 286 | "forever-agent": { 287 | "version": "0.6.1", 288 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 289 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", 290 | "dev": true 291 | }, 292 | "form-data": { 293 | "version": "2.3.3", 294 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 295 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 296 | "dev": true, 297 | "requires": { 298 | "asynckit": "^0.4.0", 299 | "combined-stream": "^1.0.6", 300 | "mime-types": "^2.1.12" 301 | } 302 | }, 303 | "get-caller-file": { 304 | "version": "2.0.5", 305 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 306 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 307 | "dev": true 308 | }, 309 | "getpass": { 310 | "version": "0.1.7", 311 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 312 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 313 | "dev": true, 314 | "requires": { 315 | "assert-plus": "^1.0.0" 316 | } 317 | }, 318 | "har-schema": { 319 | "version": "2.0.0", 320 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 321 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", 322 | "dev": true 323 | }, 324 | "har-validator": { 325 | "version": "5.1.3", 326 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 327 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 328 | "dev": true, 329 | "requires": { 330 | "ajv": "^6.5.5", 331 | "har-schema": "^2.0.0" 332 | } 333 | }, 334 | "http-signature": { 335 | "version": "1.2.0", 336 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 337 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 338 | "dev": true, 339 | "requires": { 340 | "assert-plus": "^1.0.0", 341 | "jsprim": "^1.2.2", 342 | "sshpk": "^1.7.0" 343 | } 344 | }, 345 | "is-fullwidth-code-point": { 346 | "version": "3.0.0", 347 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 348 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 349 | "dev": true 350 | }, 351 | "is-typedarray": { 352 | "version": "1.0.0", 353 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 354 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", 355 | "dev": true 356 | }, 357 | "isstream": { 358 | "version": "0.1.2", 359 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 360 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", 361 | "dev": true 362 | }, 363 | "jest-docblock": { 364 | "version": "25.1.0", 365 | "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.1.0.tgz", 366 | "integrity": "sha512-370P/mh1wzoef6hUKiaMcsPtIapY25suP6JqM70V9RJvdKLrV4GaGbfUseUVk4FZJw4oTZ1qSCJNdrClKt5JQA==", 367 | "dev": true, 368 | "requires": { 369 | "detect-newline": "^3.0.0" 370 | } 371 | }, 372 | "jsbn": { 373 | "version": "0.1.1", 374 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 375 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 376 | "dev": true 377 | }, 378 | "json-schema": { 379 | "version": "0.2.3", 380 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 381 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", 382 | "dev": true 383 | }, 384 | "json-schema-traverse": { 385 | "version": "0.4.1", 386 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 387 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 388 | "dev": true 389 | }, 390 | "json-stringify-safe": { 391 | "version": "5.0.1", 392 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 393 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", 394 | "dev": true 395 | }, 396 | "jsprim": { 397 | "version": "1.4.1", 398 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 399 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 400 | "dev": true, 401 | "requires": { 402 | "assert-plus": "1.0.0", 403 | "extsprintf": "1.3.0", 404 | "json-schema": "0.2.3", 405 | "verror": "1.10.0" 406 | } 407 | }, 408 | "locate-path": { 409 | "version": "5.0.0", 410 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", 411 | "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", 412 | "dev": true, 413 | "requires": { 414 | "p-locate": "^4.1.0" 415 | } 416 | }, 417 | "lodash": { 418 | "version": "4.17.15", 419 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 420 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", 421 | "dev": true 422 | }, 423 | "mime-db": { 424 | "version": "1.43.0", 425 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", 426 | "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", 427 | "dev": true 428 | }, 429 | "mime-types": { 430 | "version": "2.1.26", 431 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", 432 | "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", 433 | "dev": true, 434 | "requires": { 435 | "mime-db": "1.43.0" 436 | } 437 | }, 438 | "minimist": { 439 | "version": "1.2.0", 440 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 441 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 442 | "dev": true 443 | }, 444 | "ms": { 445 | "version": "2.0.0", 446 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 447 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 448 | "dev": true 449 | }, 450 | "oauth-sign": { 451 | "version": "0.9.0", 452 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 453 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", 454 | "dev": true 455 | }, 456 | "p-limit": { 457 | "version": "2.2.2", 458 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", 459 | "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", 460 | "dev": true, 461 | "requires": { 462 | "p-try": "^2.0.0" 463 | } 464 | }, 465 | "p-locate": { 466 | "version": "4.1.0", 467 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", 468 | "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", 469 | "dev": true, 470 | "requires": { 471 | "p-limit": "^2.2.0" 472 | } 473 | }, 474 | "p-try": { 475 | "version": "2.2.0", 476 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 477 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", 478 | "dev": true 479 | }, 480 | "path-exists": { 481 | "version": "4.0.0", 482 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 483 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 484 | "dev": true 485 | }, 486 | "performance-now": { 487 | "version": "2.1.0", 488 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 489 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", 490 | "dev": true 491 | }, 492 | "prettier": { 493 | "version": "1.19.1", 494 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", 495 | "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", 496 | "dev": true 497 | }, 498 | "prettier-plugin-apex": { 499 | "version": "1.2.0", 500 | "resolved": "https://registry.npmjs.org/prettier-plugin-apex/-/prettier-plugin-apex-1.2.0.tgz", 501 | "integrity": "sha512-6cD08DsmIGTj/p1FHBGQ98JhL+ocbvfGahtsczDwfTXLQbJZ3UpFY8eAfB17gLuA7Z0HwUC/1u3NWddhjYsuJQ==", 502 | "dev": true, 503 | "requires": { 504 | "axios": "^0.19.0", 505 | "jest-docblock": "^25.1.0", 506 | "wait-on": "^4.0.0", 507 | "yargs": "^15.0.1" 508 | } 509 | }, 510 | "psl": { 511 | "version": "1.7.0", 512 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", 513 | "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==", 514 | "dev": true 515 | }, 516 | "punycode": { 517 | "version": "2.1.1", 518 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 519 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 520 | "dev": true 521 | }, 522 | "qs": { 523 | "version": "6.5.2", 524 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 525 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", 526 | "dev": true 527 | }, 528 | "request": { 529 | "version": "2.88.0", 530 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 531 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 532 | "dev": true, 533 | "requires": { 534 | "aws-sign2": "~0.7.0", 535 | "aws4": "^1.8.0", 536 | "caseless": "~0.12.0", 537 | "combined-stream": "~1.0.6", 538 | "extend": "~3.0.2", 539 | "forever-agent": "~0.6.1", 540 | "form-data": "~2.3.2", 541 | "har-validator": "~5.1.0", 542 | "http-signature": "~1.2.0", 543 | "is-typedarray": "~1.0.0", 544 | "isstream": "~0.1.2", 545 | "json-stringify-safe": "~5.0.1", 546 | "mime-types": "~2.1.19", 547 | "oauth-sign": "~0.9.0", 548 | "performance-now": "^2.1.0", 549 | "qs": "~6.5.2", 550 | "safe-buffer": "^5.1.2", 551 | "tough-cookie": "~2.4.3", 552 | "tunnel-agent": "^0.6.0", 553 | "uuid": "^3.3.2" 554 | } 555 | }, 556 | "request-promise-core": { 557 | "version": "1.1.3", 558 | "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", 559 | "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", 560 | "dev": true, 561 | "requires": { 562 | "lodash": "^4.17.15" 563 | } 564 | }, 565 | "request-promise-native": { 566 | "version": "1.0.8", 567 | "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", 568 | "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", 569 | "dev": true, 570 | "requires": { 571 | "request-promise-core": "1.1.3", 572 | "stealthy-require": "^1.1.1", 573 | "tough-cookie": "^2.3.3" 574 | } 575 | }, 576 | "require-directory": { 577 | "version": "2.1.1", 578 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 579 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", 580 | "dev": true 581 | }, 582 | "require-main-filename": { 583 | "version": "2.0.0", 584 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", 585 | "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", 586 | "dev": true 587 | }, 588 | "rxjs": { 589 | "version": "6.5.4", 590 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", 591 | "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", 592 | "dev": true, 593 | "requires": { 594 | "tslib": "^1.9.0" 595 | } 596 | }, 597 | "safe-buffer": { 598 | "version": "5.2.0", 599 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 600 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", 601 | "dev": true 602 | }, 603 | "safer-buffer": { 604 | "version": "2.1.2", 605 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 606 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 607 | "dev": true 608 | }, 609 | "set-blocking": { 610 | "version": "2.0.0", 611 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 612 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", 613 | "dev": true 614 | }, 615 | "sshpk": { 616 | "version": "1.16.1", 617 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 618 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 619 | "dev": true, 620 | "requires": { 621 | "asn1": "~0.2.3", 622 | "assert-plus": "^1.0.0", 623 | "bcrypt-pbkdf": "^1.0.0", 624 | "dashdash": "^1.12.0", 625 | "ecc-jsbn": "~0.1.1", 626 | "getpass": "^0.1.1", 627 | "jsbn": "~0.1.0", 628 | "safer-buffer": "^2.0.2", 629 | "tweetnacl": "~0.14.0" 630 | } 631 | }, 632 | "stealthy-require": { 633 | "version": "1.1.1", 634 | "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", 635 | "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", 636 | "dev": true 637 | }, 638 | "string-width": { 639 | "version": "4.2.0", 640 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", 641 | "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", 642 | "dev": true, 643 | "requires": { 644 | "emoji-regex": "^8.0.0", 645 | "is-fullwidth-code-point": "^3.0.0", 646 | "strip-ansi": "^6.0.0" 647 | } 648 | }, 649 | "strip-ansi": { 650 | "version": "6.0.0", 651 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 652 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 653 | "dev": true, 654 | "requires": { 655 | "ansi-regex": "^5.0.0" 656 | } 657 | }, 658 | "tough-cookie": { 659 | "version": "2.4.3", 660 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 661 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 662 | "dev": true, 663 | "requires": { 664 | "psl": "^1.1.24", 665 | "punycode": "^1.4.1" 666 | }, 667 | "dependencies": { 668 | "punycode": { 669 | "version": "1.4.1", 670 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 671 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", 672 | "dev": true 673 | } 674 | } 675 | }, 676 | "tslib": { 677 | "version": "1.10.0", 678 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", 679 | "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", 680 | "dev": true 681 | }, 682 | "tunnel-agent": { 683 | "version": "0.6.0", 684 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 685 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 686 | "dev": true, 687 | "requires": { 688 | "safe-buffer": "^5.0.1" 689 | } 690 | }, 691 | "tweetnacl": { 692 | "version": "0.14.5", 693 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 694 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 695 | "dev": true 696 | }, 697 | "uri-js": { 698 | "version": "4.2.2", 699 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 700 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 701 | "dev": true, 702 | "requires": { 703 | "punycode": "^2.1.0" 704 | } 705 | }, 706 | "uuid": { 707 | "version": "3.4.0", 708 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 709 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", 710 | "dev": true 711 | }, 712 | "verror": { 713 | "version": "1.10.0", 714 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 715 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 716 | "dev": true, 717 | "requires": { 718 | "assert-plus": "^1.0.0", 719 | "core-util-is": "1.0.2", 720 | "extsprintf": "^1.2.0" 721 | } 722 | }, 723 | "wait-on": { 724 | "version": "4.0.0", 725 | "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-4.0.0.tgz", 726 | "integrity": "sha512-QrW3J8LzS5ADPfD9Rx5S6KJck66xkqyiFKQs9jmUTkIhiEOmkzU7WRZc+MjsnmkrgjitS2xQ4bb13hnlQnKBUQ==", 727 | "dev": true, 728 | "requires": { 729 | "@hapi/joi": "^16.1.8", 730 | "lodash": "^4.17.15", 731 | "minimist": "^1.2.0", 732 | "request": "^2.88.0", 733 | "request-promise-native": "^1.0.8", 734 | "rxjs": "^6.5.4" 735 | } 736 | }, 737 | "which-module": { 738 | "version": "2.0.0", 739 | "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", 740 | "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", 741 | "dev": true 742 | }, 743 | "wrap-ansi": { 744 | "version": "6.2.0", 745 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", 746 | "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", 747 | "dev": true, 748 | "requires": { 749 | "ansi-styles": "^4.0.0", 750 | "string-width": "^4.1.0", 751 | "strip-ansi": "^6.0.0" 752 | } 753 | }, 754 | "y18n": { 755 | "version": "4.0.0", 756 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", 757 | "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", 758 | "dev": true 759 | }, 760 | "yargs": { 761 | "version": "15.1.0", 762 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.1.0.tgz", 763 | "integrity": "sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg==", 764 | "dev": true, 765 | "requires": { 766 | "cliui": "^6.0.0", 767 | "decamelize": "^1.2.0", 768 | "find-up": "^4.1.0", 769 | "get-caller-file": "^2.0.1", 770 | "require-directory": "^2.1.1", 771 | "require-main-filename": "^2.0.0", 772 | "set-blocking": "^2.0.0", 773 | "string-width": "^4.2.0", 774 | "which-module": "^2.0.0", 775 | "y18n": "^4.0.0", 776 | "yargs-parser": "^16.1.0" 777 | } 778 | }, 779 | "yargs-parser": { 780 | "version": "16.1.0", 781 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz", 782 | "integrity": "sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==", 783 | "dev": true, 784 | "requires": { 785 | "camelcase": "^5.0.0", 786 | "decamelize": "^1.2.0" 787 | } 788 | } 789 | } 790 | } 791 | -------------------------------------------------------------------------------- /Salesforce/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "etwsblogsalesforce_sf", 3 | "version": "1.0.0", 4 | "description": "Sample SFDX ", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/eltoroit/ETScratchOrgsStarter.git" 9 | }, 10 | "keywords": [ 11 | "SFDX", 12 | "Scratch", 13 | "Starter" 14 | ], 15 | "author": "Andres Perez", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/eltoroit/ETScratchOrgsStarter/issues" 19 | }, 20 | "homepage": "https://github.com/eltoroit/ETScratchOrgsStarter#readme", 21 | "devDependencies": { 22 | "prettier": "1.19.1", 23 | "prettier-plugin-apex": "1.2.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Salesforce/sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "deploy", 5 | "default": true 6 | }, 7 | { 8 | "path": "doNotDeploy", 9 | "default": false 10 | } 11 | ], 12 | "namespace": "", 13 | "sfdcLoginUrl": "https://login.salesforce.com", 14 | "sourceApiVersion": "48.0" 15 | } 16 | -------------------------------------------------------------------------------- /WebServer/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | .env 3 | .DS_Store 4 | build/* 5 | LWC4HEROKU/* 6 | package-lock.json 7 | .sfdx/tools/apex.db 8 | -------------------------------------------------------------------------------- /WebServer/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "useTabs": true, 6 | "printWidth": 150, 7 | "overrides": [ 8 | { 9 | "files": "**/*.html", 10 | "options": { "parser": "lwc" } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /WebServer/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}/app/app.ts", 12 | "preLaunchTask": "ELTOROIT Transpile Typescript", 13 | "outFiles": ["${workspaceFolder}/build/**/*.js"] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /WebServer/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "ELTOROIT Transpile Typescript", 8 | "type": "shell", 9 | "command": "npm run prod", 10 | "group": "none", 11 | "presentation": { 12 | "reveal": "always", 13 | "panel": "dedicated", 14 | "clear": true, 15 | "focus": false, 16 | "showReuseMessage": true 17 | } 18 | }, 19 | { 20 | "label": "ELTOROIT Transpile Typescript + Execute Server", 21 | "type": "shell", 22 | "command": "npm run serve", 23 | "group": { 24 | "kind": "build", 25 | "isDefault": true 26 | }, 27 | "presentation": { 28 | "reveal": "always", 29 | "panel": "dedicated", 30 | "clear": true, 31 | "focus": false, 32 | "showReuseMessage": true 33 | } 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /WebServer/Procfile: -------------------------------------------------------------------------------- 1 | web: npm run serve -------------------------------------------------------------------------------- /WebServer/app/ETDataTypes.ts: -------------------------------------------------------------------------------- 1 | export enum MessageType { 2 | CONNECTED = 'CONNECTED', 3 | REGISTERED = 'REGISTERED', 4 | ECHO = 'ECHO', 5 | POST = 'POST', 6 | PING = 'PING', 7 | PONG = 'PONG' 8 | } 9 | 10 | export enum EventType { 11 | // When remote dyno found local dyno, we can send message here! 12 | FoundLocal = 'FoundLocal', 13 | // When the ws needs to be associated with this dyno. 14 | saveWS = 'saveWS', 15 | // When the ws needs to be removed with this dyno. 16 | deleteWS = 'deleteWS', 17 | // When a new message arrives from a websocket client 18 | messageIn = 'messageIn', 19 | // When a message is sent to a websocket client 20 | messageOut = 'messageOut', 21 | // When a new ws connection arrives, we need to register it. 22 | registerWS = 'registerWS' 23 | } 24 | 25 | export interface IWsDyno { 26 | wsId: string; 27 | dynoId: string; 28 | } 29 | 30 | export interface IMessage { 31 | type: MessageType; 32 | private: boolean; 33 | msg: string; 34 | echo: IMessage | null; 35 | dttm: Date; 36 | // FROM 37 | fromWsId: string | null; 38 | fromDyno: string; 39 | // TO 40 | toWsId: string | null; 41 | toDyno: string; 42 | } 43 | -------------------------------------------------------------------------------- /WebServer/app/ETRedis.ts: -------------------------------------------------------------------------------- 1 | import * as redis from 'redis'; 2 | import * as ETDataTypes from './ETDataTypes'; 3 | import EventEmitter from 'eventemitter3'; 4 | 5 | export default class ETRedis { 6 | eventEmitter?: EventEmitter; 7 | redisData?: redis.RedisClient; 8 | redisSubscriber?: redis.RedisClient; 9 | 10 | get dyno(): string { 11 | return process.env.DYNO ? process.env.DYNO : 'Localhost'; 12 | } 13 | 14 | constructor() { 15 | this.eventEmitter = new EventEmitter(); 16 | 17 | // REDIS Data 18 | this.redisData = this.createRedis(); 19 | this.redisData.on('error', (err: any) => { 20 | console.error(err); 21 | }); 22 | 23 | // REDIS Inter-Dyno Communication (one channel per dyno or one for localhost) 24 | this.redisSubscriber = this.createRedis(); 25 | this.redisSubscriber.subscribe(this.dyno); 26 | this.redisSubscriber.addListener('message', this.receiveMessage.bind(this)); 27 | } 28 | on(eventName: string, listener: any) { 29 | // Events: 30 | // "message" This dyno needs to handle the message published by another dyno. 31 | this.eventEmitter?.on(eventName, listener); 32 | } 33 | createRedis(): redis.RedisClient { 34 | return redis.createClient(process.env.REDIS_URL); 35 | } 36 | 37 | // REDIS Data 38 | saveWsDyno(wsId: string) { 39 | // For each Websocket, give me the dyno where it connected. 40 | // The dyno information must be recent (< 2 minutes) when reading it back 41 | // WebsocketId:string => { dyno:string, refreshedAt:Date } 42 | this.redisData?.HSET('WsId_To_DynoId', wsId, JSON.stringify({ dyno: this.dyno, refreshedAt: new Date().toJSON() })); 43 | } 44 | 45 | deleteWsDyno(wsId: string) { 46 | this.redisData?.HDEL('WsId_To_DynoId', wsId); 47 | } 48 | 49 | getOneWsDyno(wsId: string): Promise { 50 | return new Promise((resolve, reject) => { 51 | this.redisData?.HGET('WsId_To_DynoId', wsId, (err, strWsDyno) => { 52 | if (err) { 53 | reject(err); 54 | } else if (strWsDyno) { 55 | const dynoId = this.getDyno(wsId, strWsDyno); 56 | if (dynoId) { 57 | const wsDyno: ETDataTypes.IWsDyno = { wsId, dynoId }; 58 | resolve(wsDyno); 59 | } else { 60 | reject('Value was too old'); 61 | } 62 | } else { 63 | reject(`Failed HGET:WsId_To_DynoId:${wsId}`); 64 | } 65 | }); 66 | }); 67 | } 68 | 69 | getAllWsDynos(): Promise { 70 | return new Promise((resolve, reject) => { 71 | this.redisData?.HGETALL('WsId_To_DynoId', (err, strWsDynos) => { 72 | if (err) { 73 | reject(err); 74 | } else if (strWsDynos) { 75 | let outputWsDynos: { [name: string]: Array } = { 76 | same: [], 77 | other: [] 78 | }; 79 | Object.keys(strWsDynos).forEach(wsId => { 80 | const strWsDyno = strWsDynos[wsId]; 81 | const dynoId = this.getDyno(wsId, strWsDyno); 82 | if (dynoId) { 83 | const wsDyno: ETDataTypes.IWsDyno = { wsId, dynoId }; 84 | if (dynoId === this.dyno) { 85 | outputWsDynos.same.push(wsDyno); 86 | } else { 87 | outputWsDynos.other.push(wsDyno); 88 | } 89 | } 90 | }); 91 | resolve(outputWsDynos.same.concat(outputWsDynos.other)); 92 | } else { 93 | reject(`Failed HGETALL:WsId_To_DynoId`); 94 | } 95 | }); 96 | }); 97 | } 98 | 99 | getNewWsId(): Promise { 100 | return new Promise((resolve, reject) => { 101 | this.redisData?.INCR('wsid', (err, wsId) => { 102 | if (err) { 103 | reject(err); 104 | } else if (wsId) { 105 | resolve(String(wsId)); 106 | } 107 | }); 108 | }); 109 | } 110 | 111 | getDyno(wsId: string, strWsDyno: string): string { 112 | // Redis can EXPIRE the whole hash, but not each key. 113 | const now = new Date().getTime(); 114 | const jsonWsDyno = JSON.parse(strWsDyno); 115 | const refreshAt = new Date(jsonWsDyno.refreshedAt).getTime(); 116 | const age = Math.abs(now - refreshAt) / 1000; // Seconds 117 | const isRecent = age < 120; 118 | let dyno = undefined; 119 | if (!isRecent) { 120 | console.error(`Value was too old: ${strWsDyno}`); 121 | this.deleteWsDyno(wsId); 122 | } else { 123 | dyno = jsonWsDyno.dyno; 124 | } 125 | return dyno; 126 | } 127 | 128 | // REDIS Inter-Dyno Communication (one channel per dyno or one for localhost) 129 | publishMessage(toDyno: string, message: ETDataTypes.IMessage) { 130 | // We publish from "redisData" but subcribe in "redisSubscriber". Otherwise Redis will error out 131 | // ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context 132 | this.redisData?.PUBLISH(toDyno, JSON.stringify(message)); 133 | } 134 | receiveMessage(toDyno: string, strMessage: string) { 135 | const message: ETDataTypes.IMessage = JSON.parse(strMessage); 136 | console.log(`REMOTE-2 (Remote Found): Sending message ${message.fromWsId}@${message.fromDyno} => ${message.toWsId}@${message.toDyno}`); 137 | this.eventEmitter?.emit(ETDataTypes.EventType.FoundLocal, message); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /WebServer/app/ETWebsocket.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import https from 'https'; 3 | import assert from 'assert'; 4 | import * as WebSocket from 'ws'; 5 | import EventEmitter from 'eventemitter3'; 6 | import * as ETDataTypes from './ETDataTypes'; 7 | export default class ETWebsocket { 8 | wss: WebSocket.Server; 9 | eventEmitter?: EventEmitter; 10 | localWS: { [wsId: string]: WebSocket } = {}; 11 | get dyno(): string { 12 | return process.env.DYNO ? process.env.DYNO : 'Localhost'; 13 | } 14 | constructor(server: http.Server | https.Server) { 15 | this.eventEmitter = new EventEmitter(); 16 | this.wss = new WebSocket.Server({ server, clientTracking: true }); 17 | this.wss.addListener('connection', this._wssOnConnection.bind(this)); 18 | } 19 | on(eventName: string, listener: any) { 20 | this.eventEmitter?.on(eventName, listener); 21 | } 22 | wsSend(wsDyno: ETDataTypes.IWsDyno, message: ETDataTypes.IMessage) { 23 | this._wsSend(this.localWS[wsDyno.wsId], message); 24 | } 25 | registerWS(ws: WebSocket, wsId: string) { 26 | // Save websocket 27 | this._saveWS(ws, wsId); 28 | console.log(`*** ${this._debugWsId(wsId)}: New Connection`); 29 | // Register events 30 | ws.on('open', () => this._wsOnOpen(wsId)); 31 | ws.on('error', (error: Error) => this._wsOnError(error, wsId)); 32 | ws.on('message', (strMessage: string) => this._wsOnMessage(ws, strMessage, wsId)); 33 | ws.on('close', (code: Number, reason: String) => this._wsOnClose(code, reason, wsId)); 34 | // Notify client we have an Id. 35 | const message: ETDataTypes.IMessage = this._makeTestMessage(ETDataTypes.MessageType.REGISTERED, wsId); 36 | message.msg = 'REGISTERED'; 37 | this._wsSend(ws, message); 38 | } 39 | _wssOnConnection(ws: WebSocket, request: http.IncomingMessage) { 40 | // Send feedback to the incoming connection 41 | this._wsSend(ws, this._makeTestMessage(ETDataTypes.MessageType.CONNECTED)); 42 | this.eventEmitter?.emit(ETDataTypes.EventType.registerWS, ws); 43 | } 44 | _wsOnOpen(fromWsId: string) { 45 | console.log(`*** ${this._debugWsId(fromWsId)}: Connection Openned`); 46 | } 47 | _wsOnError(error: Error, fromWsId: string) { 48 | console.log(`*** ${this._debugWsId(fromWsId)}: Error`); 49 | console.log(error); 50 | } 51 | _wsOnMessage(ws: WebSocket, strMessage: string, fromWsId: string) { 52 | const message: ETDataTypes.IMessage = JSON.parse(strMessage); 53 | fromWsId = String(fromWsId); 54 | message.fromDyno = this.dyno; 55 | message.fromWsId = String(message.fromWsId); 56 | assert(message.fromWsId === fromWsId); 57 | this._saveWS(ws, fromWsId); 58 | if (message.type === ETDataTypes.MessageType.PING) { 59 | // Pong 60 | this._wsSend(ws, this._makeTestMessage(ETDataTypes.MessageType.PONG, fromWsId)); 61 | } else { 62 | this._wsSend(ws, this._makeTestMessage(ETDataTypes.MessageType.ECHO, fromWsId, message)); 63 | this.eventEmitter?.emit(ETDataTypes.EventType.messageIn, message); 64 | } 65 | } 66 | _wsOnClose(code: Number, reason: String, wsId: string) { 67 | console.log(`*** ${this._debugWsId(wsId)}: Connection Closed [${code}]: ${reason}`); 68 | delete this.localWS[wsId]; 69 | this.eventEmitter?.emit(ETDataTypes.EventType.deleteWS, wsId); 70 | } 71 | _wsSend(ws: WebSocket, message: ETDataTypes.IMessage) { 72 | ws.send(JSON.stringify(message, null, 1)); 73 | if (message.type === ETDataTypes.MessageType.POST) { 74 | this.eventEmitter?.emit(ETDataTypes.EventType.messageOut, message); 75 | } 76 | } 77 | _saveWS(ws: WebSocket, wsId: string) { 78 | this.localWS[wsId] = ws; 79 | this.eventEmitter?.emit(ETDataTypes.EventType.saveWS, wsId); 80 | } 81 | 82 | _debugWsId(wsId: string): string { 83 | return `[${wsId}@${this.dyno}]`; 84 | } 85 | _makeTestMessage(type: ETDataTypes.MessageType, wsId?: string, message?: ETDataTypes.IMessage): ETDataTypes.IMessage { 86 | const output: ETDataTypes.IMessage = { 87 | type, 88 | private: true, 89 | msg: `Test ${type}`, 90 | echo: message ? message : null, 91 | dttm: new Date(), 92 | // FROM 93 | fromWsId: wsId ? wsId : null, 94 | fromDyno: this.dyno, 95 | // TO 96 | toWsId: wsId ? wsId : null, 97 | toDyno: this.dyno 98 | }; 99 | return output; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /WebServer/app/ETWhereRUF.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import https from 'https'; 3 | import ETRedis from './ETRedis'; 4 | import * as WebSocket from 'ws'; 5 | import ETWebsocket from './ETWebsocket'; 6 | import * as ETDataTypes from './ETDataTypes'; 7 | 8 | export default class ETWhereRUF { 9 | etRedis: ETRedis; 10 | etWebsocket: ETWebsocket; 11 | 12 | get dyno(): string { 13 | return process.env.DYNO ? process.env.DYNO : 'Localhost'; 14 | } 15 | 16 | constructor(server: http.Server | https.Server) { 17 | this.etRedis = new ETRedis(); 18 | this.etWebsocket = new ETWebsocket(server); 19 | 20 | this.etRedis.on(ETDataTypes.EventType.FoundLocal, (message: ETDataTypes.IMessage) => { 21 | console.log( 22 | `REMOTE-3 (Local dyno notified): Sending message ${message.fromWsId}@${message.fromDyno} => ${message.toWsId}@${message.toDyno}` 23 | ); 24 | this.postToOne(message); 25 | }); 26 | this.etWebsocket.on(ETDataTypes.EventType.saveWS, (wsId: string) => this.saveWsDyno(wsId)); 27 | this.etWebsocket.on(ETDataTypes.EventType.deleteWS, (wsId: string) => this.deleteWsDyno(wsId)); 28 | this.etWebsocket.on(ETDataTypes.EventType.messageIn, (message: ETDataTypes.IMessage) => this.messageReceived(message)); 29 | this.etWebsocket.on(ETDataTypes.EventType.messageOut, (message: ETDataTypes.IMessage) => this.messageSent(message)); 30 | this.etWebsocket.on(ETDataTypes.EventType.registerWS, (ws: WebSocket) => this.registerWS(ws)); 31 | } 32 | 33 | saveWsDyno(wsId: string) { 34 | this.etRedis.saveWsDyno(wsId); 35 | } 36 | 37 | deleteWsDyno(wsId: string) { 38 | this.etRedis.deleteWsDyno(wsId); 39 | } 40 | 41 | registerWS(ws: WebSocket) { 42 | this.etRedis 43 | .getNewWsId() 44 | .then((wsId: string) => { 45 | this.etWebsocket.registerWS(ws, wsId); 46 | }) 47 | .catch(err => { 48 | throw new Error(err); 49 | }); 50 | } 51 | 52 | messageSent(message: ETDataTypes.IMessage) { 53 | console.log(`Sent Message: ${JSON.stringify(message)}`); 54 | } 55 | 56 | messageReceived(message: ETDataTypes.IMessage) { 57 | console.log(`Received Message: ${JSON.stringify(message)}`); 58 | 59 | switch (message.type) { 60 | case ETDataTypes.MessageType.POST: 61 | if (String(message.toWsId) === '-1') { 62 | message.private = false; 63 | this.postToAll(message); 64 | } else { 65 | message.private = true; 66 | this.postToOne(message); 67 | } 68 | break; 69 | case ETDataTypes.MessageType.PING: 70 | // Nothing 71 | break; 72 | default: 73 | break; 74 | } 75 | } 76 | 77 | postToAll(message: ETDataTypes.IMessage) { 78 | this.etRedis 79 | .getAllWsDynos() 80 | .then((wsDynos: ETDataTypes.IWsDyno[]) => { 81 | wsDynos.forEach((wsDyno: ETDataTypes.IWsDyno) => { 82 | let messageClone: ETDataTypes.IMessage = JSON.parse(JSON.stringify(message)); 83 | messageClone.toWsId = wsDyno.wsId; 84 | messageClone.toDyno = wsDyno.dynoId; 85 | this.post(messageClone, wsDyno); 86 | }); 87 | }) 88 | .catch(err => { 89 | throw new Error(err); 90 | }); 91 | } 92 | 93 | postToOne(message: ETDataTypes.IMessage) { 94 | if (message.toWsId) { 95 | this.etRedis 96 | .getOneWsDyno(message.toWsId) 97 | .then((wsDyno: ETDataTypes.IWsDyno) => { 98 | message.toDyno = wsDyno.dynoId; 99 | this.post(message, wsDyno); 100 | }) 101 | .catch(err => { 102 | throw new Error(err); 103 | }); 104 | } else { 105 | throw new Error("Can't post to the ether!"); 106 | } 107 | } 108 | 109 | post(message: ETDataTypes.IMessage, wsDyno: ETDataTypes.IWsDyno) { 110 | if (wsDyno.dynoId === this.dyno) { 111 | // Message on same dyno 112 | if (message.fromDyno === message.toDyno) { 113 | console.log(`LOCAL: Sending message ${message.fromWsId}@${message.fromDyno} => ${message.toWsId}@${message.toDyno}`); 114 | } else { 115 | console.log(`REMOTE-4 (As Local): Sending message ${message.fromWsId}@${message.fromDyno} => ${message.toWsId}@${message.toDyno}`); 116 | } 117 | this.etWebsocket.wsSend(wsDyno, message); 118 | } else { 119 | // Message on different dyno 120 | console.log(`REMOTE-1 (Find Remote): Sending message ${message.fromWsId}@${message.fromDyno} => ${message.toWsId}@${message.toDyno}`); 121 | this.etRedis.publishMessage(wsDyno.dynoId, message); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /WebServer/app/app.ts: -------------------------------------------------------------------------------- 1 | // Imports 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import http from 'http'; 5 | import https from 'https'; 6 | import dotenv from 'dotenv'; 7 | import bodyParser from 'body-parser'; 8 | import ETWhereRUF from './ETWhereRUF'; 9 | import express, { Request, Response } from 'express'; 10 | 11 | // Constants 12 | dotenv.config(); 13 | const app: express.Application = express(); 14 | const PORT: string = process.env.PORT || '5000'; 15 | const isLocalhost: boolean = PORT === '5000'; 16 | 17 | // Configure a new express application instance 18 | app.set('view engine', 'ejs'); 19 | app.set('views', path.join(__dirname + '/../', 'views')); 20 | app.use(bodyParser.json()); 21 | app.use(bodyParser.urlencoded({ extended: false })); 22 | app.use(express.static(path.join(__dirname + '/../', 'public'))); 23 | 24 | // Serve SLDS from the node-modules package 25 | app.use('/slds', express.static(path.join(__dirname, '/../node_modules/@salesforce-ux/design-system/assets/'))); 26 | 27 | app.get('/', (req, res, next) => { 28 | res.render('pages/home'); 29 | }); 30 | 31 | app.get('/ws', (req, res, next) => { 32 | res.render('pages/ws'); 33 | }); 34 | 35 | app.get('/json', (req, res, next) => { 36 | const jsonFilePath = `${path.join(__dirname, '/../public', '/data.json')}`; 37 | 38 | res.header('Content-Type', 'application/json'); 39 | res.sendFile(jsonFilePath); 40 | }); 41 | 42 | app.get('/upper', (req, res, next) => { 43 | res.send(req.query.msg.toUpperCase()); 44 | }); 45 | 46 | app.get('/dttm', (req, res, next) => { 47 | res.send(JSON.stringify(new Date()).replace(/"/g, '')); 48 | }); 49 | 50 | // curl -X POST 'http://localhost:5000/echo' -H "Content-Type: application/json" -d '{"A":1,"B":2}' 51 | app.post('/echo', (req, res, next) => { 52 | res.send(req.body); 53 | }); 54 | 55 | // Start Server(s) 56 | let etWhereRUF: ETWhereRUF; 57 | 58 | let appServer = app.listen(PORT, () => console.log(`HTTP Server: http://localhost:${PORT}/`)); 59 | if (isLocalhost) { 60 | console.log('LOCALHOST'); 61 | let portHTTPS = parseInt(PORT) + 1; 62 | const serverHTTPS = https.createServer( 63 | { 64 | key: fs.readFileSync('./key.pem'), 65 | cert: fs.readFileSync('./cert.pem') 66 | }, 67 | app 68 | ); 69 | 70 | serverHTTPS.listen(portHTTPS, () => console.log(`HTTPS Server: https://localhost:${portHTTPS}/`)); 71 | etWhereRUF = new ETWhereRUF(serverHTTPS); 72 | } else { 73 | console.log('HEROKU'); 74 | etWhereRUF = new ETWhereRUF(appServer); 75 | } 76 | -------------------------------------------------------------------------------- /WebServer/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDmjCCAoICCQDFRGVc7Ar9dTANBgkqhkiG9w0BAQsFADCBjjELMAkGA1UEBhMC 3 | Q0ExEDAOBgNVBAgMB09udGFyaW8xEDAOBgNVBAcMB01hcmtoYW0xEjAQBgNVBAoM 4 | CUVMVE9STy5JVDENMAsGA1UECwwERGVtbzESMBAGA1UEAwwJbG9jYWxob3N0MSQw 5 | IgYJKoZIhvcNAQkBFhVhcGVyZXpAc2FsZXNmb3JjZS5jb20wHhcNMjAwMjA2MTAx 6 | ODI1WhcNMjEwMjA1MTAxODI1WjCBjjELMAkGA1UEBhMCQ0ExEDAOBgNVBAgMB09u 7 | dGFyaW8xEDAOBgNVBAcMB01hcmtoYW0xEjAQBgNVBAoMCUVMVE9STy5JVDENMAsG 8 | A1UECwwERGVtbzESMBAGA1UEAwwJbG9jYWxob3N0MSQwIgYJKoZIhvcNAQkBFhVh 9 | cGVyZXpAc2FsZXNmb3JjZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 10 | AoIBAQC/lG5olOpxqo13iz0BZG3XtAzRxXmb4QN9Uz084s4F7HaJzFCMsIh4ZymV 11 | 6LUhbJ0j4wCpkUlZwYrzitBu5tWYwDSBCHwIpTCrHjo73ZqafHbsQ8ViisDcXPmE 12 | W+RJTCEKx9xv/ilelHWU7fgk9JTx6OjoivYgTlGUASh0Ln3lk0t1Ctn5TSBil9jp 13 | P8hWh33VvigQA/bTVtLdg3JRJmtsV5otLkYJsGfQZ2v7bcBPoz/hNnm7J9r0Rdwc 14 | gh1Odtiv3BUV40cazRHF6wncL3EZuAA6qnKnitx8lBa9j8vEc0hs6Xh6Q5JyorGH 15 | M/fzkruQ5HYpwftzGc1R3flw/kk1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAH6s 16 | 3R6n39jmB/gvPVosAO4zvFzrDstOMr6Dl5q6CjMpyAa/9osW8l97TG83alYnXtoB 17 | 04qqw/HkyGxz4b2nO0xMGhu9QyxuIRzsRJ1s4j8hg2Uh/syN3VXqC/AJKqnVvnyS 18 | kVqr5S40KDUF3MB/Ak7WNsKfQZ+lEoe8Hs9t3spSc/I3EwznxtBhEdbWZQhfSsnu 19 | j4mfhazNeNQr80bjuiTrJXP9A9gxoOle5QP7MuXQAH3IeExaTrfrIl4SJPH0iHNs 20 | RVsDoYNt5EKzkKP8IcM25Pmy6mlSmb0w83o1h9DlHixH1CksJd2Vp9iVFYTgXtE0 21 | 4rKnLj+Db/lIC0vAebw= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /WebServer/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAv5RuaJTqcaqNd4s9AWRt17QM0cV5m+EDfVM9POLOBex2icxQ 3 | jLCIeGcplei1IWydI+MAqZFJWcGK84rQbubVmMA0gQh8CKUwqx46O92amnx27EPF 4 | YorA3Fz5hFvkSUwhCsfcb/4pXpR1lO34JPSU8ejo6Ir2IE5RlAEodC595ZNLdQrZ 5 | +U0gYpfY6T/IVod91b4oEAP201bS3YNyUSZrbFeaLS5GCbBn0Gdr+23AT6M/4TZ5 6 | uyfa9EXcHIIdTnbYr9wVFeNHGs0RxesJ3C9xGbgAOqpyp4rcfJQWvY/LxHNIbOl4 7 | ekOScqKxhzP385K7kOR2KcH7cxnNUd35cP5JNQIDAQABAoIBABB8raPCbQqrKtGG 8 | 4hEkm29cx12mKPkJPEukxjhfLGvGFc12D9iLuLLj7ehAxpXByyQzfPMa1b0rstGS 9 | uK1SoD8tC1evaz0zOiT/zPMp9wwkefeLPYlld4N/XNnPLq5AJvfOi2H39Yd2L0hJ 10 | 0nkbs1W+PY0FqHjPRKYAE4MsHRmMlSkL1U7+z21hEbJxzCBUUoYaijr3oDXScX7q 11 | 5gk/szNDbzQfzOtWJGJPh5QzJDcEq6lAQbVUjyFstXZ2UWFuPzkag/+nWRn71W3m 12 | lhpBxlynEiiuD/+glFgOd//wW7a9OMKqE0vKUdFeLzSutdHhIqZ9nTmvS/dv/lge 13 | jAYHvAECgYEA+OousrRK/zr1gZoDNbwxyGYcCKhJOJqP/YCSxws67oPfI32tgsgV 14 | dVU5sCz5NJ0P+2Nnty7pjECJibXKVs689QZndtqTfBGdU8aJc4Bj71pEl9J4Sou5 15 | 8Ll7Me0X7tu0rDOlS1CmEbz6Xv41UlVt0Dqp7P6+BYsI2Ws1eMhLVmECgYEAxQh0 16 | YCaurvHeqTccrtRp6fp6hwH4sUPpuVdLOW2aT2ItjH4p7fZM45KalYJwiFTZl2CU 17 | hr4RzeLEXGy4JUjpba1e9l8b1SSPRP/s2AuNBaHKnFg1ZNrKkVDzIHA5mn/CoJYY 18 | scxmx7a/LEcAWVXuPt8c9FIG+N5yAbHIodu6e1UCgYBCuIsh7b/4oDY0sjLQEY0K 19 | mYxEKc/Os2eLXQ56+iCm1IRYgBSlbLqLw/d9qOB34O3qxZ+Ec7e4l4gGeMsu76Wo 20 | zDmpmzdTV2ljjmFDq5OeEgU26PzDhqalxyIlpNb4eL36NrHE+W46bPxzwBJM6odP 21 | /JmV5EbNN5J8rQcdRBsloQKBgEizo+1OmKooyRX7JfREoIgbSlbCnXcbLgSbd0BL 22 | duLPwSrT6rjJZvmZwLxmEwva8ybuFh/ZxkwH3piT7Sakzq0Pibiyw1xUTyEQpd3Z 23 | 9UEcv9wMmDcMnC4ehndzsW2WssP6XkZMu6f5gHTgBfrwwCYmwVJNahEuUzbY2MUQ 24 | hLC5AoGBAJvaS/k586eaKK+nDY9AsSgMaYIBDlJJht6Lag8+t8B8cGimTDivRkIH 25 | 4ujolMiSTF43OcnI+jhHyObdn1iKLuVLf3h6a+T2fzB/Ru5BfLQIj2n+K2xH/4bK 26 | nBkdWXAFibTVfQtUA0kaJEcoFk2yoI2ctptDO4PM16gnfFZcgrfF 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /WebServer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "etwsblogsalesforce_web", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "tsc": "tsc", 8 | "prod": "tsc", 9 | "serve": "ts-node-dev --respawn --transpileOnly ./app/app.ts" 10 | }, 11 | "author": "Andres Perez (http://eltoro.it)", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@salesforce-ux/design-system": "^2.9.3", 15 | "@types/ejs": "^2.6.3", 16 | "@types/express": "^4.17.2", 17 | "@types/redis": "^2.8.14", 18 | "@types/ws": "^6.0.4", 19 | "dotenv": "^8.2.0", 20 | "ejs": "^2.6.1", 21 | "eventemitter3": "^4.0.0", 22 | "express": "^4.17.1", 23 | "redis": "^2.8.0", 24 | "ts-node-dev": "^1.0.0-pre.40", 25 | "typescript": "^3.7.4", 26 | "ws": "^7.2.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /WebServer/public/THLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eltoroit/ETWSBlogSalesforce/92ebb5097a9d3cab3f4b4fe3f0e539845bff3b80/WebServer/public/THLogo.png -------------------------------------------------------------------------------- /WebServer/public/data.json: -------------------------------------------------------------------------------- 1 | { "a": 1, "b": 2, "c": 3 } 2 | -------------------------------------------------------------------------------- /WebServer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "target": "es5", 7 | /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 8 | "module": "commonjs", 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | /* Generates corresponding '.map' file. */ 16 | "sourceMap": true, 17 | // "outFile": "./", /* Concatenate and emit output to single file. */ 18 | /* Redirect output structure to the directory. */ 19 | "outDir": "./build", 20 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 21 | // "composite": true, /* Enable project compilation */ 22 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | // 29 | // 30 | // 31 | /* Strict Type-Checking Options */ 32 | /* Enable all strict type-checking options. */ 33 | "strict": true, 34 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 35 | // "strictNullChecks": true, /* Enable strict null checks. */ 36 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 37 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 38 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 39 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 40 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 41 | // 42 | // 43 | // 44 | /* Additional Checks */ 45 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 46 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 47 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 48 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 49 | // 50 | // 51 | // 52 | /* Module Resolution Options */ 53 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 54 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 55 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 56 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 57 | // "typeRoots": [], /* List of folders to include type definitions from. */ 58 | // "types": [], /* Type declaration files to be included in compilation. */ 59 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 60 | /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 61 | "esModuleInterop": true, 62 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 63 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 64 | // 65 | // 66 | // 67 | /* Source Map Options */ 68 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 69 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 70 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 71 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 72 | // 73 | // 74 | // 75 | /* Experimental Options */ 76 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 77 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 78 | } 79 | } -------------------------------------------------------------------------------- /WebServer/views/pages/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%- include('../partials/header.ejs', {title: 'Heroku demo'}) %> 5 | 11 | 12 | 13 | 14 |
15 | <% if (locals.dttm) { %> <%- locals.dttm %> <% } %> 16 | 17 |
18 |
19 |
20 |
21 |
22 | 23 |
24 |
 
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /WebServer/views/pages/ws.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%- include('../partials/header.ejs', {title: 'WebSocket demo'}) %> 5 | 21 | 22 | 23 | 24 |
25 |

A MORE COMPLEX DEMO HAS BEEN BUILT ON SALESFORCE!

26 |

Websocket Tester

27 | 28 |

Server

29 |

30 | 34 |

35 |

36 | 37 |

38 |

39 | 40 | 41 |

42 |

Force re-open

43 |

Keep alive

44 | 45 |

Message

46 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 64 | 65 | 66 | 67 | 70 | 71 |
From Id: 50 | 51 |
To Id: 56 | 57 |
Message: 62 | 63 |
68 | 69 |
72 | 73 |

History

74 |

75 | 76 |

77 |


78 | 79 | 80 | 81 | 82 | 83 | 84 |
MessageAgeDTTM
85 |
86 | 87 | 88 | 303 | 304 | -------------------------------------------------------------------------------- /WebServer/views/partials/header.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= title %> 6 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "etwsblogsalesforce", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "postinstall": "npm install --prefix WebsServer" 6 | } 7 | } 8 | --------------------------------------------------------------------------------