better idle and error handling
This commit is contained in:
+22
-3
@@ -8,13 +8,13 @@ import (
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"git.stephensearles.com/stephen/acedoc"
|
||||
@@ -62,17 +62,20 @@ type CaddyHugo struct {
|
||||
|
||||
func (ch *CaddyHugo) ObserveLTime(ltime uint64) uint64 {
|
||||
ch.mtx.Lock()
|
||||
defer ch.mtx.Unlock()
|
||||
|
||||
if ch.ltime < ltime {
|
||||
ch.ltime = ltime
|
||||
}
|
||||
|
||||
ch.mtx.Unlock()
|
||||
return ch.LTime()
|
||||
}
|
||||
|
||||
func (ch *CaddyHugo) LTime() uint64 {
|
||||
return atomic.AddUint64(&ch.ltime, 1)
|
||||
ch.mtx.Lock()
|
||||
defer ch.mtx.Unlock()
|
||||
ch.ltime++
|
||||
return ch.ltime
|
||||
}
|
||||
|
||||
func (ch *CaddyHugo) Setup(c *caddy.Controller) error {
|
||||
@@ -261,6 +264,7 @@ func (ch *CaddyHugo) doc(r *http.Request) (*docref, error) {
|
||||
defer ch.mtx.Unlock()
|
||||
|
||||
name := r.URL.Path[len("/hugo/edit/"):]
|
||||
name = filepath.Join(ch.Site.Root, name)
|
||||
|
||||
_, ok := ch.docs[name]
|
||||
if !ok {
|
||||
@@ -363,7 +367,20 @@ func (ch *CaddyHugo) DeltaWebsocket(w http.ResponseWriter, r *http.Request) (int
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
const idlePing = 15 * time.Second
|
||||
const idlePingShort = 1 * time.Millisecond
|
||||
var timer *time.Timer
|
||||
|
||||
timer = time.AfterFunc(idlePing, func() {
|
||||
conn.WriteJSON(Message{
|
||||
Deltas: []acedoc.Delta{},
|
||||
LTime: ch.LTime(),
|
||||
})
|
||||
timer.Reset(idlePing)
|
||||
})
|
||||
|
||||
client := doc.doc.Client(acedoc.DeltaHandlerFunc(func(ds []acedoc.Delta) error {
|
||||
timer.Reset(idlePing)
|
||||
err := conn.WriteJSON(Message{
|
||||
Deltas: ds,
|
||||
LTime: ch.LTime(),
|
||||
@@ -390,11 +407,13 @@ func (ch *CaddyHugo) DeltaWebsocket(w http.ResponseWriter, r *http.Request) (int
|
||||
|
||||
fmt.Println(message)
|
||||
ch.ObserveLTime(message.LTime)
|
||||
timer.Reset(idlePingShort)
|
||||
|
||||
err = client.PushDeltas(message.Deltas...)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+144
-43
@@ -17,10 +17,19 @@ func (t tmplData) Content() ([]string, error) {
|
||||
var files []string
|
||||
|
||||
err := filepath.Walk(path.Join(t.Site.Root, "content"), func(name string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
name, err = filepath.Rel(t.Site.Root, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
files = append(files, name)
|
||||
return nil
|
||||
})
|
||||
@@ -34,6 +43,33 @@ func (t tmplData) Content() ([]string, error) {
|
||||
}
|
||||
|
||||
func (t tmplData) ContentTypes() ([]string, error) {
|
||||
nameMap := map[string]struct{}{"default": struct{}{}}
|
||||
|
||||
names, err := t.contentTypes(path.Join(t.Site.Root, "archetypes"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, name := range names {
|
||||
nameMap[name] = struct{}{}
|
||||
}
|
||||
|
||||
names, err = t.contentTypes(path.Join(t.Site.Root, "themes", "hugo-theme-minos", "archetypes"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, name := range names {
|
||||
nameMap[name] = struct{}{}
|
||||
}
|
||||
|
||||
var out []string
|
||||
for name := range nameMap {
|
||||
out = append(out, name[:len(name)-len(filepath.Ext(name))])
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (t tmplData) contentTypes(dir string) ([]string, error) {
|
||||
layoutDir, err := os.Open(path.Join(t.Site.Root, "archetypes"))
|
||||
if err != nil {
|
||||
fmt.Println("opening layout dir", err)
|
||||
@@ -47,12 +83,7 @@ func (t tmplData) ContentTypes() ([]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := []string{"default"}
|
||||
for _, name := range names {
|
||||
out = append(out, name[:len(name)-len(filepath.Ext(name))])
|
||||
}
|
||||
|
||||
return out, nil
|
||||
return names, nil
|
||||
}
|
||||
|
||||
type tmplData struct {
|
||||
@@ -109,28 +140,64 @@ var EditPage = `<html>
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/hugo/simplemde.css" />
|
||||
<script src="https://unpkg.com/vue"></script>
|
||||
<script src="https://unpkg.com/moment"></script>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="container" >
|
||||
<div id="lastSaved">
|
||||
<span v-if="ltime >serverLtime && (sendQueue.length > 0 || sentRecently.length > 0)">last saved ${ lastSaved.from(now) }, saving</span>
|
||||
<span v-else>saved</span>
|
||||
<span v-if="connectionError">, ${connectionError}</span>
|
||||
</div>
|
||||
<textarea id="editor">{{ .LoadContent }}</textarea>
|
||||
<div id="draft"><iframe src="{{ .IframeSource }}">Loading draft...</iframe></div>
|
||||
<div id="draft"><!--<iframe src="{{ .IframeSource }}">Loading draft...</iframe>--></div>
|
||||
</div>
|
||||
<script>
|
||||
var ltime = 0;
|
||||
var uiBindings = {
|
||||
ltime: 0,
|
||||
serverLtime: 0,
|
||||
lastSaved: moment(),
|
||||
now: moment(),
|
||||
connectionError: null,
|
||||
sendQueue: [],
|
||||
sentRecently: [],
|
||||
};
|
||||
|
||||
var app = new Vue({
|
||||
el: "#container",
|
||||
data: uiBindings,
|
||||
delimiters: ["${", "}"],
|
||||
});
|
||||
|
||||
function getLtime() {
|
||||
ltime++
|
||||
return ltime
|
||||
uiBindings.ltime++
|
||||
return uiBindings.ltime
|
||||
}
|
||||
|
||||
function observeServer(l) {
|
||||
uiBindings.serverLtime = l;
|
||||
while (uiBindings.sentRecently.length > 0 && uiBindings.sentRecently[0].ltime < l) {
|
||||
uiBindings.sentRecently.pop();
|
||||
}
|
||||
observe(l);
|
||||
}
|
||||
|
||||
function observe(l) {
|
||||
if (l > ltime) {
|
||||
ltime = l;
|
||||
if (l > uiBindings.ltime) {
|
||||
uiBindings.now = moment();
|
||||
uiBindings.lastSaved = moment();
|
||||
uiBindings.ltime = l;
|
||||
}
|
||||
}
|
||||
|
||||
var editorElem = document.getElementById("editor");
|
||||
var editor = new SimpleMDE({element: editorElem, forceSync: true});
|
||||
|
||||
|
||||
// Create WebSocket connection.
|
||||
const socket = new WebSocket('ws://' + location.host + location.pathname);
|
||||
var socket = connect();
|
||||
|
||||
var iframe = document.querySelector("#draft > iframe");
|
||||
|
||||
@@ -144,39 +211,76 @@ var EditPage = `<html>
|
||||
iframe.contentWindow.location.reload();
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
uiBindings.now = moment();
|
||||
if (uiBindings.connectionError) {
|
||||
socket = connect();
|
||||
} else if (uiBindings.sendQueue.length > 0) {
|
||||
var ltime = getLtime();
|
||||
socket.send(JSON.stringify({
|
||||
"deltas": uiBindings.sendQueue,
|
||||
"ltime": ltime,
|
||||
}));
|
||||
uiBindings.sentRecently.push({
|
||||
"ltime": ltime,
|
||||
"sent": uiBindings.sendQueue,
|
||||
});
|
||||
uiBindings.sendQueue = [];
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Listen for messages
|
||||
socket.addEventListener('message', function (event) {
|
||||
var message = JSON.parse(event.data);
|
||||
observe(message.ltime);
|
||||
function connect() {
|
||||
const socket = new WebSocket((location.protocol == "https:" ? 'wss://' : 'ws://') + location.host + location.pathname);
|
||||
|
||||
var deltas = [];
|
||||
deltas.push.apply(deltas, message.deltas);
|
||||
// Listen for messages
|
||||
socket.addEventListener('message', function (event) {
|
||||
var message = JSON.parse(event.data);
|
||||
observeServer(message.ltime);
|
||||
|
||||
deltas.forEach(function(aceDelta) {
|
||||
var cmDelta = aceDeltaToCM(aceDelta)
|
||||
console.log(cmDelta);
|
||||
var deltas = [];
|
||||
deltas.push.apply(deltas, message.deltas);
|
||||
|
||||
var content = ""
|
||||
var to = {
|
||||
line: aceDelta.start.row,
|
||||
ch: aceDelta.start.column,
|
||||
}
|
||||
deltas.forEach(function(aceDelta) {
|
||||
var cmDelta = aceDeltaToCM(aceDelta)
|
||||
console.log(cmDelta);
|
||||
|
||||
if (aceDelta.action == "insert") {
|
||||
content = aceDelta.lines.join("\n");
|
||||
to = null;
|
||||
}
|
||||
var content = ""
|
||||
var to = {
|
||||
line: aceDelta.start.row,
|
||||
ch: aceDelta.start.column,
|
||||
}
|
||||
|
||||
editor.codemirror.doc.replaceRange(content, cmDelta.from, to, "dontreflect");
|
||||
sawChanges = sawChangesBumpsTo;
|
||||
})
|
||||
});
|
||||
if (aceDelta.action == "insert") {
|
||||
content = aceDelta.lines.join("\n");
|
||||
to = null;
|
||||
}
|
||||
|
||||
socket.addEventListener('error', function (err) {
|
||||
console.log(err);
|
||||
});
|
||||
editor.codemirror.doc.replaceRange(content, cmDelta.from, to, "dontreflect");
|
||||
sawChanges = sawChangesBumpsTo;
|
||||
})
|
||||
});
|
||||
|
||||
socket.addEventListener('open', function () {
|
||||
uiBindings.connectionError = null;
|
||||
});
|
||||
|
||||
socket.addEventListener('close', function () {
|
||||
if (!uiBindings.connectionError) {
|
||||
getLtime();
|
||||
uiBindings.connectionError = "server connection closed, reconnecting...";
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener('error', function (err) {
|
||||
if (!uiBindings.connectionError) {
|
||||
uiBindings.connectionError = err;
|
||||
getLtime();
|
||||
}
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
editor.codemirror.on("change", function (cm, cmDelta) {
|
||||
if (cmDelta.origin == "dontreflect") {
|
||||
@@ -187,10 +291,7 @@ var EditPage = `<html>
|
||||
console.log(cmDelta, "=>", aceDelta)
|
||||
|
||||
sawChanges = sawChangesBumpsTo;
|
||||
socket.send(JSON.stringify({
|
||||
"deltas": [aceDelta],
|
||||
"ltime": getLtime(),
|
||||
}))
|
||||
uiBindings.sendQueue.push(aceDelta);
|
||||
})
|
||||
|
||||
function cmDeltaToAce(cmDelta) {
|
||||
|
||||
@@ -2,4 +2,5 @@ localhost:8080 {
|
||||
hugo
|
||||
root ./testsite
|
||||
errors { * }
|
||||
pprof
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user