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