better idle and error handling

pull/8/head
Stephen Searles 7 years ago
parent d683b94c77
commit 4a5a20e49e
  1. 25
      caddyhugo.go
  2. 199
      templates.go
  3. 1
      testdir/caddyfile

@ -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
}
}
}

@ -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);
// Listen for messages
socket.addEventListener('message', function (event) {
var message = JSON.parse(event.data);
observe(message.ltime);
var deltas = [];
deltas.push.apply(deltas, message.deltas);
deltas.forEach(function(aceDelta) {
var cmDelta = aceDeltaToCM(aceDelta)
console.log(cmDelta);
var content = ""
var to = {
line: aceDelta.start.row,
ch: aceDelta.start.column,
}
if (aceDelta.action == "insert") {
content = aceDelta.lines.join("\n");
to = null;
}
editor.codemirror.doc.replaceRange(content, cmDelta.from, to, "dontreflect");
sawChanges = sawChangesBumpsTo;
})
});
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);
function connect() {
const socket = new WebSocket((location.protocol == "https:" ? 'wss://' : 'ws://') + location.host + location.pathname);
// Listen for messages
socket.addEventListener('message', function (event) {
var message = JSON.parse(event.data);
observeServer(message.ltime);
var deltas = [];
deltas.push.apply(deltas, message.deltas);
deltas.forEach(function(aceDelta) {
var cmDelta = aceDeltaToCM(aceDelta)
console.log(cmDelta);
var content = ""
var to = {
line: aceDelta.start.row,
ch: aceDelta.start.column,
}
if (aceDelta.action == "insert") {
content = aceDelta.lines.join("\n");
to = null;
}
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);
});
socket.addEventListener('error', function (err) {
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
}

Loading…
Cancel
Save