├── .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 | Real Time Demo
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 |
13 | one:utilityBarTemplateDesktop
14 |
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 |
12 | flexipage:defaultAppHomeTemplate
13 |
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 |
2 |
3 | {title}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {state.name}
27 | {state.count}
28 |
29 |
30 |
31 |
32 |
33 |
34 | {postData.message}
35 |
36 | {postCounter}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Client Id
49 | Publish
50 | Received
51 | Buttons
52 |
53 |
54 |
55 |
56 |
64 |
65 |
66 |
67 |
68 |
69 | {clientDataRefresh}
70 | {postCounter}
71 |
72 |
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 | Demo
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 | From Id
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 | Message
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 | Private
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 | To Id
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 | Type
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 | WSS
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 | Dashboard
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 |
31 | Heroku
32 | Localhost
33 |
34 |
35 |
36 |
37 |
38 |
39 | Close
40 | Open
41 |
42 |
Force re-open
43 |
Keep alive
44 |
45 |
Message
46 |
72 |
73 |
History
74 |
75 | Clear
76 |
77 |
78 |
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 |
--------------------------------------------------------------------------------