Skip to content

Commit 120c506

Browse files
authored
Merge pull request #2 from theopensystemslab/peters-changes
Reusable ShareDB client prototype
2 parents 60fcdc0 + e1de1da commit 120c506

File tree

9 files changed

+1493
-1573
lines changed

9 files changed

+1493
-1573
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
/.pnp
66
.pnp.js
77

8+
/public/flow.json
9+
810
# testing
911
/coverage
1012

package.json

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,27 @@
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6-
"@nx-js/observer-util": "^4.2.2",
76
"@teamwork/websocket-json-stream": "^2.0.0",
8-
"@testing-library/jest-dom": "^4.2.4",
9-
"@testing-library/react": "^9.3.2",
10-
"@testing-library/user-event": "^7.1.2",
117
"@types/jest": "^24.0.0",
128
"@types/node": "^12.0.0",
13-
"@types/react": "^16.9.0",
14-
"@types/react-dom": "^16.9.0",
15-
"comlink": "^4.3.0",
9+
"@types/react": "^16.9.43",
10+
"@types/react-dom": "^16.9.8",
11+
"@types/uuid": "^8.0.0",
1612
"express": "^4.17.1",
17-
"nanoid": "^3.1.10",
1813
"random-words": "^1.1.1",
19-
"react": "^16.13.1",
20-
"react-dom": "^16.13.1",
14+
"react": "^0.0.0-experimental-4c8c98ab9",
15+
"react-dom": "^0.0.0-experimental-4c8c98ab9",
16+
"react-router-dom": "^5.2.0",
2117
"react-scripts": "3.4.1",
22-
"react-use-comlink": "^2.0.1",
2318
"reconnecting-websocket": "^4.4.0",
2419
"sharedb": "^1.4.0",
25-
"typescript": "~3.7.2",
20+
"typescript": "^3.9.7",
21+
"uuid": "^8.2.0",
2622
"ws": "^7.3.1"
2723
},
24+
"devDependencies": {
25+
"concurrently": "^5.2.0"
26+
},
2827
"scripts": {
2928
"start": "concurrently 'node server' 'react-scripts start'",
3029
"build": "react-scripts build",
@@ -45,10 +44,5 @@
4544
"last 1 firefox version",
4645
"last 1 safari version"
4746
]
48-
},
49-
"devDependencies": {
50-
"concurrently": "^5.2.0",
51-
"ts-node": "^8.10.2",
52-
"ts-node-dev": "^1.0.0-pre.52"
5347
}
5448
}

src/App.tsx

Lines changed: 193 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,225 @@
11
import randomWords from "random-words";
2-
import React, { useEffect, useRef } from "react";
2+
import { v4 as uuid } from "uuid";
3+
import React, { useMemo, useState, useEffect, useCallback } from "react";
34
import { connectToDB, getConnection } from "./sharedb";
5+
import { useTransition } from "./react-experimental";
6+
import {
7+
Link,
8+
BrowserRouter as Router,
9+
useHistory,
10+
useLocation,
11+
} from "react-router-dom";
412

5-
const loadNewFlow = () => {
6-
window.location.href = [window.location.origin, randomWords()].join("#");
7-
// should react to url change rather than full reload
8-
window.location.reload();
13+
interface Node {
14+
text: string;
15+
}
16+
17+
type Flow = {
18+
nodes: Record<string, Node>;
19+
edges: Array<[string | null, string]>;
920
};
1021

11-
const Flow: React.FC<{ id: string }> = ({ id }) => {
12-
const [state, setState] = React.useState<any>();
13-
const docRef = useRef(null);
22+
// Custom hook for talking to a flow in ShareDB
23+
function useFlow(config: {
24+
id: string;
25+
}): {
26+
state: Flow | null;
27+
addNode: () => void;
28+
removeNode: (id: string) => void;
29+
reset: (flow: Flow) => void;
30+
isPending: boolean;
31+
} {
32+
// Setup
1433

15-
useEffect(() => {
16-
// should probably useContext or something rather than
17-
// this useRef stuff
18-
docRef.current = getConnection(id);
19-
const doc = docRef.current;
34+
const [startTransition, isPending] = useTransition();
35+
36+
const [state, setState] = useState<Flow | null>(null);
37+
38+
const doc = useMemo(() => getConnection(config.id), [config.id]);
2039

40+
useEffect(() => {
2141
const cloneStateFromShareDB = () =>
22-
setState(JSON.parse(JSON.stringify(doc.data)));
42+
startTransition(() => {
43+
setState(JSON.parse(JSON.stringify(doc.data)));
44+
});
2345

2446
connectToDB(doc).then(() => {
2547
cloneStateFromShareDB();
2648
doc.on("op", cloneStateFromShareDB);
2749
});
2850

2951
return () => {
30-
docRef.current.destroy();
52+
setState(null);
53+
doc.destroy();
3154
};
32-
}, []);
55+
}, [doc, startTransition]);
56+
57+
// Methods
58+
59+
const addNode = useCallback(() => {
60+
doc.submitOp([{ p: ["nodes", uuid()], oi: { text: randomWords() } }]);
61+
}, [doc]);
62+
63+
const removeNode = useCallback(
64+
(id) => {
65+
doc.submitOp([{ p: ["nodes", id], od: {} }]);
66+
},
67+
[doc]
68+
);
69+
70+
const reset = useCallback(
71+
(flow) => {
72+
doc.submitOp([{ p: [], od: doc.data, oi: flow }]);
73+
},
74+
[doc]
75+
);
76+
77+
// Public API
78+
79+
return {
80+
state,
81+
addNode,
82+
removeNode,
83+
reset,
84+
isPending,
85+
};
86+
}
87+
88+
const Flow: React.FC<{ id: string }> = ({ id }) => {
89+
const flow = useFlow({ id });
90+
91+
if (flow.state === null) {
92+
return <p>Loading...</p>;
93+
}
3394

3495
return (
35-
<div>
36-
{state?.nodes &&
37-
Object.keys(state.nodes).map((k) => (
38-
<Node key={k} doc={docRef.current} id={k} />
96+
<>
97+
<main>
98+
<button
99+
onClick={() => {
100+
flow.addNode();
101+
}}
102+
>
103+
Add
104+
</button>
105+
<button
106+
onClick={() => {
107+
fetch("/flow.json")
108+
.then((res) => res.json())
109+
.then((flowData) => {
110+
flow.reset(flowData);
111+
});
112+
}}
113+
>
114+
Import flow
115+
</button>
116+
<button
117+
onClick={() => {
118+
flow.reset({
119+
nodes: {},
120+
edges: [],
121+
});
122+
}}
123+
>
124+
Reset
125+
</button>
126+
{Object.keys(flow.state.nodes).map((k) => (
127+
<NodeView
128+
key={k}
129+
onRemove={flow.removeNode}
130+
id={k}
131+
node={flow.state.nodes[k]}
132+
/>
39133
))}
134+
</main>
135+
{flow.isPending && <div className="overlay" />}
136+
</>
137+
);
138+
};
139+
140+
const NodeView = React.memo(
141+
({
142+
id,
143+
node,
144+
onRemove,
145+
}: {
146+
id: string;
147+
node: Node;
148+
onRemove: (id: string) => void;
149+
}) => (
150+
<div className="node">
40151
<button
152+
className="remove-button"
41153
onClick={() => {
42-
// add a node
43-
docRef.current.submitOp([{ p: ["nodes", randomWords()], oi: {} }]);
154+
onRemove(id);
44155
}}
45156
>
46-
ADD
157+
×
47158
</button>
48-
49-
<button onClick={loadNewFlow}>New flow</button>
159+
<p>
160+
{node.text || "unset"} {Math.round(Math.random() * 1000)}
161+
</p>
50162
</div>
163+
),
164+
(prevProps, nextProps) =>
165+
prevProps.id === nextProps.id &&
166+
prevProps.onRemove === nextProps.onRemove &&
167+
JSON.stringify(prevProps.node) === JSON.stringify(nextProps.node)
168+
);
169+
170+
const SimpleLink = ({ to }: { to: string }) => {
171+
const location = useLocation();
172+
return (
173+
<Link to={to}>{location.hash === to ? <strong>{to}</strong> : to}</Link>
51174
);
52175
};
53176

54-
const Node = React.memo(({ id, doc }: any) => (
55-
<h1
56-
onClick={() => {
57-
// remove the node
58-
doc.submitOp([{ p: ["nodes", id], od: doc.data.nodes[id] }]);
59-
}}
60-
>
61-
{id} {Math.round(Math.random() * 1000)}
62-
</h1>
63-
));
64-
65177
const App = () => {
66-
let [, id] = window.location.href.split("#");
67-
if (!id) loadNewFlow();
178+
const history = useHistory();
179+
const location = useLocation();
180+
181+
const id = useMemo(() => {
182+
if (location.hash.length < 2) {
183+
return null;
184+
}
185+
return location.hash.slice(1);
186+
}, [location]);
187+
188+
// If there is no ID readable from the hash, redirect to a freshly created one
189+
useEffect(() => {
190+
if (id === null) {
191+
history.push(`#${randomWords()}`);
192+
}
193+
}, [id, history]);
68194

69-
return <Flow id={id} />;
195+
if (id === null) {
196+
return <p>Redirecting...</p>;
197+
}
198+
199+
return (
200+
<div>
201+
<nav>
202+
<SimpleLink to="#direct" />
203+
<SimpleLink to="#captain" />
204+
<button
205+
onClick={() => {
206+
history.push(`#${randomWords()}`);
207+
}}
208+
>
209+
New flow
210+
</button>
211+
</nav>
212+
<Flow id={id} />
213+
</div>
214+
);
215+
};
216+
217+
const Container = () => {
218+
return (
219+
<Router>
220+
<App />
221+
</Router>
222+
);
70223
};
71224

72-
export default App;
225+
export default Container;

src/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import React from "react";
2-
import ReactDOM from "react-dom";
2+
import { createRoot } from "./react-experimental";
33
import App from "./App";
44
import * as serviceWorker from "./serviceWorker";
5+
import "./style.css";
56

6-
ReactDOM.render(
7+
createRoot(document.getElementById("root")).render(
78
<React.StrictMode>
89
<App />
9-
</React.StrictMode>,
10-
document.getElementById("root")
10+
</React.StrictMode>
1111
);
1212

1313
// If you want your app to work offline and load faster, you can change

src/react-app-env.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
/// <reference types="react-scripts" />
2+
/// <reference types="react-dom/experimental" />
3+
/// <reference types="react/experimental" />

src/react-experimental.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as React from "react";
2+
import * as ReactDOM from "react-dom";
3+
4+
/**
5+
* Typing hacks for experimental concurrent mode features in React
6+
*/
7+
8+
export const createRoot = (ReactDOM as any).unstable_createRoot;
9+
10+
export const useTransition: () => [
11+
(fn: Function) => void,
12+
boolean
13+
] = (React as any).unstable_useTransition;

0 commit comments

Comments
 (0)