Skip to content

Commit fb99241

Browse files
committed
caldera
0 parents  commit fb99241

35 files changed

+5421
-0
lines changed

Diff for: .eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src/generated/*.js

Diff for: .eslintrc.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module.exports = {
2+
parser: "@typescript-eslint/parser",
3+
extends: [
4+
"react-app",
5+
"prettier/@typescript-eslint",
6+
"plugin:prettier/recommended"
7+
],
8+
parserOptions: {
9+
ecmaVersion: 2019,
10+
sourceType: "module",
11+
ecmaFeatures: {
12+
jsx: true
13+
}
14+
},
15+
settings: {
16+
react: {
17+
version: "detect"
18+
}
19+
}
20+
};

Diff for: .gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
dist/
2+
public/
3+
node_modules/
4+
.DS_Store
5+
db/

Diff for: .prettierrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "tabWidth": 2, "singleQuote": false }

Diff for: .vscode/settings.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"editor.formatOnSave": true,
3+
"typescript.tsdk": "node_modules/typescript/lib",
4+
"eslint.enable": true,
5+
"eslint.validate": [
6+
"javascript",
7+
"javascriptreact",
8+
{ "language": "typescript", "autoFix": true },
9+
{ "language": "typescriptreact", "autoFix": true }
10+
],
11+
"eslint.autoFixOnSave": true,
12+
"editor.codeActionsOnSave": {
13+
"source.fixAll.eslint": true
14+
}
15+
}

Diff for: docs/architecture.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Caldera server architecture
2+
3+
Caldera is a custom React renderer built on top of the React Reconciler API. Instead of mutating a real DOM stored on the server, it sends the mutations over WebSockets to a client which performs them on the real DOM. I haven't benchmarked the current renderer, but it maintains essentially the same amount of state as a simple no-op renderer and was able to run 250000 concurrent instances (all running timers) in 1.7GB of real heap. This should make it reasonably safe to maintain disconnected instances in memory for a non-trivial amount of time, and is way cheaper than maintaining a real DOM (JSDOM, undom, or similar) on top of the React virtual dom.
4+
5+
## Philosophy
6+
7+
In one sentence: maintain as little non-React state on the server as possible. This means:
8+
9+
1. All component state should be in the React tree - this means all inputs are controlled (https://reactjs.org/docs/forms.html)
10+
2. Don't maintain any tree - the virtual dom already contains everything necessary for operation
11+
3. Punt serialization/hydration as far back as possible - because Fiber instances are really cheap, disconnected sessions can be maintained for quite a while.

Diff for: docs/event-propagation.md

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Event propagation
2+
3+
React exposes an event abstraction which includes a feature subset of the real DOM event system. The constraints are namely:
4+
5+
- At most one listener for an event type is registered for any given node
6+
- Event propagation bubbles _up_ the tree and doesn't _capture_ down the tree (to do capture, pass in stuff like onClickCapture - this is uncommonly used and we don't need to support right now but the method is similar)
7+
8+
To support this abstraction on the server without a real DOM while avoiding the n+1 problem, the propagation path to walk is handled on the client (we will deal with validation later, but this is unlikely to pose a security issue). This works as follows:
9+
10+
1. When a callback is registered, preventDefault if the event is _cancelable_ but do not stopPropagation. Ignore and stopPropagation if isTrusted is not true.
11+
2. When an event is dispatched, store the current node ID at each callback handler when hit in an array (single element if `event.bubbles` is false). Send the contents of that array over RPC when the event hits `document.body`, along with the node ID of the original target element.
12+
3. On the server, we walk up the callbacks of the corresponding server node instances until stopPropagation is called. When stopPropagation is called or all nodes in the list are processed, if preventDefault was not called and the original event was _cancelable_ we send a virtual event dispatch message to the client, and the client permits the side-effect but stops propagation (as detailed in step 1).
13+
14+
Ideally we would be able to implement a useful subset of the [SyntheticEvent](https://reactjs.org/docs/events.html) wrapper.
15+
16+
## Cancelabiity
17+
18+
- Non-trusted DOM events (save for `click`) don't cause default side-effects when dispatched. We work around this by supporting only a subset of cancellable events that have a corresponding redispatch method (currently submit, focus, blur, click). React Flare is relying on a similar type of async cancellation/default preventDefaulted, so it's worth investigating their implementation (https://github.com/facebook/react/issues/15257)
19+
20+
## Potential validation methods
21+
22+
- Use some ID generation scheme that lets you easily check if a node is a child of another node (harder)
23+
- Pass reference to instance up the tree with registered callback (easier, but updating this may be harder)
24+
- Walk React Fiber instance parent references (slow, may need to walk entire app tree per event process, but updating is handled automatically)

0 commit comments

Comments
 (0)