fixes, eventSource

This commit is contained in:
Ilya Kantor 2019-06-16 22:30:52 +03:00
parent 70cf2b219d
commit 027933531e
5 changed files with 429 additions and 48 deletions

View file

@ -16,7 +16,7 @@ A value of this type can be created using `Symbol()`:
let id = Symbol(); let id = Symbol();
``` ```
We can also give symbol a description (also called a symbol name), mostly useful for debugging purposes: Upon creation, we can give symbol a description (also called a symbol name), mostly useful for debugging purposes:
```js run ```js run
// id is a symbol with the description "id" // id is a symbol with the description "id"
@ -74,7 +74,7 @@ alert(id.description); // id
Symbols allow us to create "hidden" properties of an object, that no other part of code can occasionally access or overwrite. Symbols allow us to create "hidden" properties of an object, that no other part of code can occasionally access or overwrite.
For instance, if we want to store an "identifier" for the object `user`, we can use a symbol as a key for it: For instance, if we'd like to add an "identifier" to the object `user`, we can use a symbol as a key for it:
```js run ```js run
let user = { name: "John" }; let user = { name: "John" };
@ -88,7 +88,7 @@ What's the benefit of using `Symbol("id")` over a string `"id"`?
Let's make the example a bit deeper to see that. Let's make the example a bit deeper to see that.
Imagine that another script wants to have its own "id" property inside `user`, for its own purposes. That may be another JavaScript library, so the scripts are completely unaware of each other. Imagine that another script wants to have its own identifier inside `user`, for its own purposes. That may be another JavaScript library, so thes scripts are completely unaware of each other.
Then that script can create its own `Symbol("id")`, like this: Then that script can create its own `Symbol("id")`, like this:
@ -99,9 +99,9 @@ let id = Symbol("id");
user[id] = "Their id value"; user[id] = "Their id value";
``` ```
There will be no conflict, because symbols are always different, even if they have the same name. There will be no conflict between our and their identifiers, because symbols are always different, even if they have the same name.
Now note that if we used a string `"id"` instead of a symbol for the same purpose, then there *would* be a conflict: ...But if we used a string `"id"` instead of a symbol for the same purpose, then there *would* be a conflict:
```js run ```js run
let user = { name: "John" }; let user = { name: "John" };
@ -117,7 +117,7 @@ user.id = "Their id value"
### Symbols in a literal ### Symbols in a literal
If we want to use a symbol in an object literal, we need square brackets. If we want to use a symbol in an object literal `{...}`, we need square brackets around it.
Like this: Like this:
@ -155,7 +155,7 @@ for (let key in user) alert(key); // name, age (no symbols)
alert( "Direct: " + user[id] ); alert( "Direct: " + user[id] );
``` ```
That's a part of the general "hiding" concept. If another script or a library loops over our object, it won't unexpectedly access a symbolic property. `Object.keys(user)` also ignores them. That's a part of the general "hiding symbolic properties" principle. If another script or a library loops over our object, it won't unexpectedly access a symbolic property.
In contrast, [Object.assign](mdn:js/Object/assign) copies both string and symbol properties: In contrast, [Object.assign](mdn:js/Object/assign) copies both string and symbol properties:
@ -190,13 +190,13 @@ alert( obj[0] ); // test (same property)
## Global symbols ## Global symbols
As we've seen, usually all symbols are different, even if they have the same names. But sometimes we want same-named symbols to be same entities. As we've seen, usually all symbols are different, even if they have the same name. But sometimes we want same-named symbols to be same entities.
For instance, different parts of our application want to access symbol `"id"` meaning exactly the same property. For instance, different parts of our application want to access symbol `"id"` meaning exactly the same property.
To achieve that, there exists a *global symbol registry*. We can create symbols in it and access them later, and it guarantees that repeated accesses by the same name return exactly the same symbol. To achieve that, there exists a *global symbol registry*. We can create symbols in it and access them later, and it guarantees that repeated accesses by the same name return exactly the same symbol.
In order to create or read a symbol in the registry, use `Symbol.for(key)`. In order to read (create if absent) a symbol from the registry, use `Symbol.for(key)`.
That call checks the global registry, and if there's a symbol described as `key`, then returns it, otherwise creates a new symbol `Symbol(key)` and stores it in the registry by the given `key`. That call checks the global registry, and if there's a symbol described as `key`, then returns it, otherwise creates a new symbol `Symbol(key)` and stores it in the registry by the given `key`.
@ -206,7 +206,7 @@ For instance:
// read from the global registry // read from the global registry
let id = Symbol.for("id"); // if the symbol did not exist, it is created let id = Symbol.for("id"); // if the symbol did not exist, it is created
// read it again // read it again (maybe from another part of the code)
let idAgain = Symbol.for("id"); let idAgain = Symbol.for("id");
// the same symbol // the same symbol
@ -266,14 +266,14 @@ Other symbols will also become familiar when we study the corresponding language
`Symbol` is a primitive type for unique identifiers. `Symbol` is a primitive type for unique identifiers.
Symbols are created with `Symbol()` call with an optional description. Symbols are created with `Symbol()` call with an optional description (name).
Symbols are always different values, even if they have the same name. If we want same-named symbols to be equal, then we should use the global registry: `Symbol.for(key)` returns (creates if needed) a global symbol with `key` as the name. Multiple calls of `Symbol.for` return exactly the same symbol. Symbols are always different values, even if they have the same name. If we want same-named symbols to be equal, then we should use the global registry: `Symbol.for(key)` returns (creates if needed) a global symbol with `key` as the name. Multiple calls of `Symbol.for` with the same `key` return exactly the same symbol.
Symbols have two main use cases: Symbols have two main use cases:
1. "Hidden" object properties. 1. "Hidden" object properties.
If we want to add a property into an object that "belongs" to another script or a library, we can create a symbol and use it as a property key. A symbolic property does not appear in `for..in`, so it won't be occasionally listed. Also it won't be accessed directly, because another script does not have our symbol, so it will not occasionally intervene into its actions. If we want to add a property into an object that "belongs" to another script or a library, we can create a symbol and use it as a property key. A symbolic property does not appear in `for..in`, so it won't be occasionally processed together with other properties. Also it won't be accessed directly, because another script does not have our symbol. So the property will be protected from occasional use or overwrite.
So we can "covertly" hide something into objects that we need, but others should not see, using symbolic properties. So we can "covertly" hide something into objects that we need, but others should not see, using symbolic properties.

View file

@ -18,82 +18,96 @@ try {
Fetch fails, as expected. Fetch fails, as expected.
## Why? ## Why? A brief history
Because cross-origin restrictions protect the internet from evil hackers. Because cross-origin restrictions protect the internet from evil hackers.
Seriously. Let's make a very brief historical digression. Seriously. Let's make a very brief historical digression.
For many years JavaScript did not have any special methods to perform network requests. **For many years a script from one site could not access the content of another site.**
**A script from one site could not access the content of another site.**
That simple, yet powerful rule was a foundation of the internet security. E.g. a script from the page `hacker.com` could not access user's mailbox at `gmail.com`. People felt safe. That simple, yet powerful rule was a foundation of the internet security. E.g. a script from the page `hacker.com` could not access user's mailbox at `gmail.com`. People felt safe.
But web developers demanded more power. A variety of tricks were invented to work around it. JavaScript also did not have any special methods to perform network requests at that time. It was a toy language to decorate a web page.
But web developers demanded more power. A variety of tricks were invented to work around the limitation.
### Using forms
One way to communicate with another server was to submit a `<form>` there. People submitted it into `<iframe>`, just to stay on the current page, like this: One way to communicate with another server was to submit a `<form>` there. People submitted it into `<iframe>`, just to stay on the current page, like this:
```html ```html
<!-- form target --> <!-- form target -->
*!*
<iframe name="iframe"></iframe> <iframe name="iframe"></iframe>
*/!*
<!-- a form could be dynamically generated and submited by JavaScript --> <!-- a form could be dynamically generated and submited by JavaScript -->
*!*
<form target="iframe" method="POST" action="http://another.com/…"> <form target="iframe" method="POST" action="http://another.com/…">
*/!*
... ...
</form> </form>
``` ```
- So, it was possible to make a GET/POST request to another site, even without networking methods. So, it was possible to make a GET/POST request to another site, even without networking methods. But as it's forbidden to access the content of an `<iframe>` from another site, it wasn't possible to read the response.
- But as it's forbidden to access the content of an `<iframe>` from another site, it wasn't possible to read the response.
So, `<form>` allowed to submit the data anywhere, but the response content was unaccessible. ...Okay, in fact there actually were tricks for that (required special scripts at both remote and our page), but let's not delve deeper. Nothing good in those for us now.
### Using scripts
Another trick was to use a `<script src="http://another.com/…">` tag. A script could have any `src`, from any domain. But again -- it was impossible to access the raw content of such script. Another trick was to use a `<script src="http://another.com/…">` tag. A script could have any `src`, from any domain. But again -- it was impossible to access the raw content of such script.
If `another.com` intended to expose data for this kind of access, then a so-called "JSONP (JSON with padding)" protocol was used. If `another.com` intended to expose data for this kind of access, then a so-called "JSONP (JSON with padding)" protocol was used.
Here's the flow: Let's say we need to get the data from `http://another.com` this way:
1. First, in advance, we declare a global function to accept the data, e.g. `gotWeather`. 1. First, in advance, we declare a global function to accept the data, e.g. `gotWeather`.
2. Then we make a `<script>` and pass its name as the `callback` query parameter, e.g. `src="http://another.com/weather.json?callback=gotWeather"`.
3. The remote server dynamically generates a response that wraps the data into `gotWeather(...)` call. ```js
// 1. Declare the function to process the data
function gotWeather({ temperature, humidity }) {
alert(`temperature: ${temperature}, humidity: ${humidity}`);
}
```
2. Then we make a `<script>` tag with `src="http://another.com/weather.json?callback=gotWeather"`, please note that the name of our function is its `callback` parameter.
```js
let script = document.createElement('script');
script.src = `http://another.com/weather.json?callback=gotWeather`;
document.body.append(script);
```
3. The remote server dynamically generates a script that calls `gotWeather(...)` with the data it wants us to receive.
```js
// The expected answer from the server looks like this:
gotWeather({
temperature: 25,
humidity: 78
});
```
4. As the script executes, `gotWeather` runs, and, as it's our function, we have the data. 4. As the script executes, `gotWeather` runs, and, as it's our function, we have the data.
Here's an example of the code to receive the data in JSONP:
```js run
// 1. Declare the function to process the data
function gotWeather({ temperature, humidity }) {
alert(`temperature: ${temperature}, humidity: ${humidity}`);
}
// 2. Pass its name as the ?callback parameter for the script
let script = document.createElement('script');
script.src = `https://cors.javascript.info/article/fetch-crossorigin/demo/script?callback=gotWeather`;
document.body.append(script);
// 3. The expected answer from the server looks like this:
/*
gotWeather({
temperature: 25,
humidity: 78
});
*/
```
That works, and doesn't violate security, because both sides agreed to pass the data this way. And, when both sides agree, it's definitely not a hack. There are still services that provide such access, as it works even for very old browsers. That works, and doesn't violate security, because both sides agreed to pass the data this way. And, when both sides agree, it's definitely not a hack. There are still services that provide such access, as it works even for very old browsers.
After a while, modern network methods appeared. At first, cross-origin requests were forbidden. But as a result of long discussions, cross-domain requests were allowed, in a way that does not add any capabilities unless explicitly allowed by the server. After a while, networking methods appeared, such as `XMLHttpRequest`.
At first, cross-origin requests were forbidden. But as a result of long discussions, cross-domain requests were allowed, in a way that does not add any capabilities unless explicitly allowed by the server.
## Simple requests ## Simple requests
[Simple requests](http://www.w3.org/TR/cors/#terminology) must satisfy the following conditions: There are two types of cross-domain requests:
1. Simple requests.
2. All the others.
Simple Requests are, well, simpler to make, so let's start with them.
A [simple request](http://www.w3.org/TR/cors/#terminology) is a request that satisfies two conditions:
1. [Simple method](http://www.w3.org/TR/cors/#simple-method): GET, POST or HEAD 1. [Simple method](http://www.w3.org/TR/cors/#simple-method): GET, POST or HEAD
2. [Simple headers](http://www.w3.org/TR/cors/#simple-header) -- only allowed: 2. [Simple headers](http://www.w3.org/TR/cors/#simple-header) -- the only allowed custom headers are:
- `Accept`, - `Accept`,
- `Accept-Language`, - `Accept-Language`,
- `Content-Language`, - `Content-Language`,

View file

@ -0,0 +1,270 @@
# Server Sent Events
The [Server-Sent Events](https://html.spec.whatwg.org/multipage/comms.html#the-eventsource-interface) specification describes a built-in class `EventSource`, that keeps connection with the server and allows to receive events from it.
Similar to `WebSocket`, the connection is persistent.
But there are several important differences:
| `WebSocket` | `EventSource` |
|-------------|---------------|
| Bi-directional: both client and server can exchange messages | One-directional: only server sends data |
| Binary and text data | Only text |
| WebSocket protocol | Regular HTTP |
`EventSource` is a less-powerful way of communicating with the server than `WebSocket`.
Why should one ever use it?
The main reason: it's simpler. In many applications, the power of `WebSocket` is a little bit too much.
We need to receve a stream of data from server: maybe chat messages or market prices, or whatever. That's what `EventSource` is good at. Also it supports auto-reconnect, something we need to implement manually with `WebSocket`. Besides, it's a plain old HTTP, not a new protocol.
## Getting messages
To start receiving messages, we just need to create `new EventSource(url)`.
The browser will connect to `url` and keep the connection open, waiting for events.
The server should respond with status 200 and the header `Content-Type: text/event-stream`, then keep the connection and write messages into it in the special format, like this:
```
data: Message 1
data: Message 2
data: Message 3
data: of two lines
```
- A message text goes after `data:`, the space after the semicolon is optional.
- Messages are delimited with double line breaks `\n\n`.
- To send a line break `\n`, we can immediately one more `data:` (3rd message above).
In practice, complex messages are usually sent JSON-encoded, so line-breaks are encoded within them.
For instance:
```js
data: {"user":"John","message":"First line*!*\n*/!* Second line"}
```
...So we can assume that one `data:` holds exactly one message.
For each such message, the `message` event is generated:
```js
let eventSource = new EventSource("/events/subscribe");
eventSource.onmessage = function(event) {
console.log("New message", event.data);
// will log 3 times for the data stream above
};
// or eventSource.addEventListener('message', ...)
```
### Cross-domain requests
`EventSource` supports cross-origin requests, like `fetch` any other networking methods. We can use any URL:
```js
let source = new EventSource("https://another-site.com/events");
```
The remote server will get the `Origin` header and must respond with `Access-Control-Allow-Origin` to proceed.
To pass credentials, we should set the additional option `withCredentials`, like this:
```js
let source = new EventSource("https://another-site.com/events", {
withCredentials: true
});
```
Please see the chapter <info:fetch-crossorigin> for more details about cross-domain headers.
## Reconnection
Upon creation, `new EventSource` connects to the server, and if the connection is broken -- reconnects.
That's very convenient, as we don't have to care about it.
There's a small delay between reconnections, a few seconds by default.
The server can set the recommended delay using `retry:` in response (in milliseconds):
```js
retry: 15000
data: Hello, I set the reconnection delay to 15 seconds
```
The `retry:` may come both together with some data, or as a standalone message.
The browser should wait that much before reconnect. If the network connection is lost, the browser may wait till it's restored, and then retry.
- If the server wants the browser to stop reconnecting, it should respond with HTTP status 204.
- If the browser wants to close the connection, it should call `eventSource.close()`:
```js
let eventSource = new EventSource(...);
eventSource.close();
```
Also, there will be no reconnection if the response has an incorrect `Content-Type` or its HTTP status differs from 301, 307, 200 and 204. The connection the `"error"` event is emitted, and the browser won't reconnect.
```smart
There's no way to "reopen" a closed connection. If we'd like to connect again, just create a new `EventSource`.
```
## Message id
When a connection breaks due to network problems, either side can't be sure which messages were received, and which weren't.
To correctly resume the connection, each message should have an `id` field, like this:
```
data: Message 1
id: 1
data: Message 2
id: 2
data: Message 3
data: of two lines
id: 3
```
When a message with `id:` is received, the browser:
- Sets the property `eventSource.lastEventId` to its value.
- Upon reconnection sends the header `Last-Event-ID` with that `id`, so that the server may re-send following messages.
```smart header="Put `id:` after `data:`"
Please note: the `id:` is appended below the message data, to ensure that `lastEventId` is updated after the message data is received.
```
## Connection status: readyState
The `EventSource` object has `readyState` property, that has one of three values:
```js no-beautify
EventSource.CONNECTING = 0; // connecting or reconnecting
EventSource.OPEN = 1; // connected
EventSource.CLOSED = 2; // connection closed
```
When an object is created, or the connection is down, it's always `EventSource.CONNECTING` (equals `0`).
We can query this property to know the state of `EventSource`.
## Event types
By default `EventSource` object generates three events:
- `message` -- a message received, available as `event.data`.
- `open` -- the connection is open.
- `error` -- the connection could not be established, e.g. the server returned HTTP 500 status.
The server may specify another type of event with `event: ...` at the event start.
For example:
```
event: join
data: Bob
data: Hello
event: leave
data: Bob
```
To handle custom events, we must use `addEventListener`, not `onmessage`:
```js
eventSource.addEventListener('join', event => {
alert(`Joined ${event.data}`);
});
eventSource.addEventListener('message', event => {
alert(`Said: ${event.data}`);
});
eventSource.addEventListener('leave', event => {
alert(`Left ${event.data}`);
});
```
## Full example
Here's the server that sends messages with `1`, `2`, `3`, then `bye` and breaks the connection.
Then the browser automatically reconnects.
[codetabs src="eventsource"]
## Summary
The `EventSource` object communicates with the server. It establishes a persistent connection and allows the server to send messages over it.
It offers:
- Automatic reconnect, with tunable `retry` timeout.
- Message ids to resume events, the last identifier is sent in `Last-Event-ID` header.
- The current state is in the `readyState` property.
That makes `EventSource` a viable alternative to `WebSocket`, as it's more low-level and lacks these features.
In many real-life applications, the power of `EventSource` is just enough.
Supported in all modern browsers (not IE).
The syntax is:
```js
let source = new EventSource(url, [credentials]);
```
The second argument has only one possible option: `{ withCredentials: true }`, it allows sending cross-domain credentials.
Overall cross-domain security is same as for `fetch` and other network methods.
### Properties of an `EventSource` object
`readyState`
: The current connection state: either `EventSource.CONNECTING (=0)`, `EventSource.OPEN (=1)` or `EventSource.CLOSED (=2)`.
`lastEventId`
: The last received `id`. Upon reconnection the browser sends it in the header `Last-Event-ID`.
### Methods
`close()`
: Closes the connection соединение.
### Events
`message`
: Message received, the data is in `event.data`.
`open`
: The connection is established.
`error`
: In case of an error, including both lost connection (will auto-reconnect) and fatal errors. We can check `readyState` to see if the reconnection is being attempted.
The server may set a custom event name in `event:`. Such events should be handled using `addEventListener`, not `on<event>`.
### Server response format
The server sends messages, delimited by `\n\n`.
Message parts may start with:
- `data:` -- message body, a sequence of multiple `data` is interpreted as a single message, with `\n` between the parts.
- `id:` -- renews `lastEventId`, sent in `Last-Event-ID` on reconnect.
- `retry:` -- recommends a retry delay for reconnections in ms. There's no way to set it from JavaScript.
- `event:` -- even name, must precede `data:`.

View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<script>
let eventSource;
function start() { // when "Start" button pressed
if (!window.EventSource) {
// IE or an old browser
alert("The browser doesn't support EventSource.");
return;
}
eventSource = new EventSource('digits');
eventSource.onopen = function(e) {
log("Event: open");
};
eventSource.onerror = function(e) {
log("Event: error");
if (this.readyState == EventSource.CONNECTING) {
log(`Reconnecting (readyState=${this.readyState})...`);
} else {
log("Error has occured.");
}
};
eventSource.addEventListener('bye', function(e) {
log("Event: bye, data: " + e.data);
});
eventSource.onmessage = function(e) {
log("Event: message, data: " + e.data);
};
}
function stop() { // when "Stop" button pressed
eventSource.close();
log("eventSource.close()");
}
function log(msg) {
logElem.innerHTML += msg + "<br>";
document.documentElement.scrollTop = 99999999;
}
</script>
<button onclick="start()">Start</button> Press the "Start" to begin.
<div id="logElem" style="margin: 6px 0"></div>
<button onclick="stop()">Stop</button> "Stop" to finish.

View file

@ -0,0 +1,47 @@
let http = require('http');
let url = require('url');
let querystring = require('querystring');
function onDigits(req, res) {
res.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache'
});
let i = 0;
let timer = setInterval(write, 1000);
write();
function write() {
i++;
if (i == 4) {
res.write('event: bye\ndata: bye-bye\n\n');
clearInterval(timer);
res.end();
return;
}
res.write('data: ' + i + '\n\n');
}
}
function accept(req, res) {
if (req.url == '/digits') {
onDigits(req, res);
return;
}
fileServer.serve(req, res);
}
if (!module.parent) {
http.createServer(accept).listen(8080);
} else {
exports.accept = accept;
}