diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 802bfc9..d1ceb21 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -1,5 +1,5 @@ -import vdom, karax, karaxdsl, jstrutils, components, localstorage +import vdom, karax, karaxdsl, jstrutils, components, karaxdb/client type Filter = enum @@ -9,6 +9,12 @@ var selectedEntry = -1 filter: Filter entriesLen: int + data: Db + +registerOnUpdate proc(newDb: Db) = + merge(newDb, data) + data = newDb + redraw() const contentSuffix = cstring"content" @@ -16,25 +22,23 @@ const lenSuffix = cstring"entriesLen" proc getEntryContent(pos: int): cstring = - result = getItem(&pos & contentSuffix) - if result == cstring"null": - result = nil + extract(data, &pos, contentSuffix) proc isCompleted(pos: int): bool = - var value = getItem(&pos & completedSuffix) + var value = extract(data, &pos, completedSuffix) result = value == cstring"true" proc setEntryContent(pos: int, content: cstring) = - setItem(&pos & contentSuffix, content) + insert(data, &pos, contentSuffix, content) proc markAsCompleted(pos: int, completed: bool) = - setItem(&pos & completedSuffix, &completed) + insert(data, &pos, completedSuffix, &completed) proc addEntry(content: cstring, completed: bool) = setEntryContent(entriesLen, content) markAsCompleted(entriesLen, completed) inc entriesLen - setItem(lenSuffix, &entriesLen) + insert(data, lenSuffix, "equals", &entriesLen) proc updateEntry(pos: int, content: cstring, completed: bool) = setEntryContent(pos, content) @@ -61,7 +65,7 @@ proc toggleEntry(ev: Event; n: VNode) = markAsCompleted(id, not isCompleted(id)) proc onAllDone(ev: Event; n: VNode) = - clear() + insert(data, lenSuffix, "equals", "0") selectedEntry = -1 proc clearCompleted(ev: Event, n: VNode) = @@ -150,8 +154,5 @@ setOnHashChange(proc(hash: cstring) = elif hash == "#/active": filter = active ) -if hasItem(lenSuffix): - entriesLen = parseInt getItem(lenSuffix) -else: - entriesLen = 0 +entriesLen = 0 setRenderer createDom diff --git a/src/kajax.nim b/src/kajax.nim index be230f0..2d88cd1 100644 --- a/src/kajax.nim +++ b/src/kajax.nim @@ -48,3 +48,21 @@ proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)]; proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".} proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".} + +type + MessageEvent* {.importc.} = ref object + data*: cstring + ErrorEvent* {.importc.} = ref object + CloseEvent* {.importc.} = ref object + code*: int + reason*: cstring + + WebSocket* {.importc.} = ref object + onopen*: proc() + onmessage*: proc(ev: MessageEvent) + onclose*: proc(ev: CloseEvent) + onerror*: proc(ev: ErrorEvent) + +proc newWebsocket*(url, protocol: cstring): WebSocket {.importc: "new WebSocket".} + +proc send*(socket: WebSocket, data: cstring) {.importcpp.} diff --git a/src/karaxdb/client.nim b/src/karaxdb/client.nim new file mode 100644 index 0000000..7ea648b --- /dev/null +++ b/src/karaxdb/client.nim @@ -0,0 +1,99 @@ + +import "../kajax", "../jdict", common +export common +from "../karax" import kout + +type + Db* = ref object + data: seq[Triple] + version*: int + next: Db + + Query* = object + constraints*: set[TripleKind] + t*: Triple + + Message {.importc.} = ref object + kind: MessageKind + data: seq[Triple] + version: int + id: MessageId +# RequestMessage {.importc.} = ref object + +let conn = newWebSocket("ws://localhost:8080", "karaxdb") +var gid: MessageId +var pendingIds = newJDict[MessageId, Message]() +var pending: int + +#proc loadDb*(url: cstring): Db = +# result = nil + +proc newTransaction*(): Db = + result = Db(data: @[]) + +proc insert*(head, newdb: Db) = + newdb.next = head + #result = newdb + let expectedVersion = if not head.isNil: head.version + 1 else: 1 + inc gid.int + let m = Message(kind: NewData, data: newdb.data, version: expectedVersion, id: gid) + pendingIds[gid] = m + inc pending + conn.send(toJson(m)) + +proc merge*(newer, older: Db) = + newer.next = older + +proc registerOnUpdate*(update: proc(db: Db)) = + conn.onmessage = + proc (e: MessageEvent) = + let msg = fromJson[Message](e.data) + case msg.kind + of Conflict: + # conflict, so throw away the sent data, don't apply the changes: + if pending > 0: + pendingIds.del msg.id + dec pending + # server sent data that caused the conflict: + if msg.data.len > 0: + let db = Db(data: msg.data, version: msg.version) + update(db) + of Accepted: + kout cstring"accepted", pending + if pending > 0: + let d = pendingIds[msg.id] + # data was not submitted again, so we use the in-memory version + # of the data: + let db = Db(data: d.data, version: d.version) + pendingIds.del msg.id + dec pending + update(db) + of Newdata: + let db = Db(data: msg.data, version: msg.version) + update(db) + +iterator list*(db: Db, q: Query): Triple = + # XXX here the datamodel comes in! We must not + # yield outdated data! + var it = db + while it != nil: + for d in it.data: + var match = true + for k in q.constraints: + if d[k] != q.t[k]: match = false + if match: yield d + it = it.next + +proc extract*(db: Db, q: Query): Triple = + for x in list(db, q): + result = x + break + +proc extract*(db: Db, subj, pred: kstring): kstring = + let q = Query(constraints: {Subj, Pred}, t: [subj, pred, ""]) + result = extract(db, q)[Obj] + +proc insert*(db: Db, subj, pred, obj: kstring) = + let result = Db() + result.data.add([subj, pred, obj]) + insert(db, result) diff --git a/src/karaxdb/common.nim b/src/karaxdb/common.nim new file mode 100644 index 0000000..67cdf4c --- /dev/null +++ b/src/karaxdb/common.nim @@ -0,0 +1,17 @@ + +when defined(js): + type kstring* = cstring +else: + type kstring* = string + +type + TripleKind* = enum + Subj, Pred, Obj + + DbValue* = kstring + Triple* = array[TripleKind, DbValue] + + MessageId* = distinct int + + MessageKind* = enum + Newdata, Conflict, Accepted diff --git a/src/karaxdb/server.nim b/src/karaxdb/server.nim new file mode 100644 index 0000000..c0291d4 --- /dev/null +++ b/src/karaxdb/server.nim @@ -0,0 +1,56 @@ + +import asynchttpserver, asyncdispatch, asyncnet, "../../../websocket/websocket", common, json + +type + Message = object + kind: MessageKind + id: MessageId + data: seq[Triple] + version: int + +proc `%`(id: MessageId): JsonNode = %BiggestInt(id) +proc `%`(k: MessageKind): JsonNode = %BiggestInt(k) + +proc triplesFromJson(j: JsonNode): seq[Triple] = + result = newSeq[Triple](j.len) + var i = 0 + for t in j: + doAssert t.kind == JArray + result[i] = [t[0].str, t[1].str, t[2].str] + inc i + +proc messageFromJson(j: JsonNode): Message = + Message(kind: MessageKind(j["kind"].num), id: MessageId(j["id"].num), + data: triplesFromJson(j["data"]), version: j["version"].num.int) + +var server = newAsyncHttpServer() + +proc cb(req: Request) {.async.} = + let (success, error) = await(verifyWebsocketRequest(req, "karaxdb")) + if not success: + echo "WS negotiation failed: " & error + await req.respond(Http400, "Websocket negotiation failed: " & error) + req.client.close + else: + echo "New websocket customer arrived!" + while true: + try: + var f = await req.client.readData(false) + echo "(opcode: " & $f.opcode & ", data: " & $f.data.len & ")" + let m = messageFromJson(f.data.parseJson) + let om = Message(kind: Accepted, id: m.id, data: @[], version: m.version) + let oms = $(%*om) + echo "OUTPUT ", oms + if f.opcode == Opcode.Text: + waitFor req.client.sendText(oms, false) + else: + echo "protocol error" + #waitFor req.client.sendBinary(f.data, false) + except: + echo getCurrentExceptionMsg() + break + + req.client.close() + echo ".. socket went away." + +waitFor server.serve(Port(8080), cb)