Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ws + httprequest for mac (and linux) #74

Merged
merged 2 commits into from
Feb 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions examples/websocket.nim
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ import windy
# If there is any error, onError will be called during
# the next pollEvents (or later).

let req = openWebSocket("wss://stream.pushbullet.com/websocket/test")
let ws = openWebSocket("wss://stream.pushbullet.com/websocket/test")

req.onError = proc(msg: string) =
ws.onError = proc(msg: string) =
echo "onError: " & msg

req.onOpen = proc() =
ws.onOpen = proc() =
echo "onOpen"

req.onMessage = proc(msg: string, kind: WebSocketMessageKind) =
ws.onMessage = proc(msg: string, kind: WebSocketMessageKind) =
echo "onMessage: ", msg

req.onClose = proc() =
ws.onClose = proc() =
echo "onClose"

# Closing the window exits the demo
Expand Down
6 changes: 6 additions & 0 deletions src/windy/common.nim
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ type
const
defaultHttpDeadline*: float32 = -1

proc `==`*(a, b: HttpRequestHandle): bool =
a.int == b.int

proc `==`*(a, b: WebSocketHandle): bool =
a.int == b.int

proc `[]`*(buttonView: ButtonView, button: Button): bool =
button in set[Button](buttonView)

Expand Down
216 changes: 216 additions & 0 deletions src/windy/http.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import common, internal, std/asyncdispatch, std/httpclient, std/random, std/strutils, std/tables, ws, std/times

type
HttpRequestState* = ref object
url*, verb*: string
headers*: seq[HttpHeader]
requestBody*: string
deadline*: float64
canceled*: bool

onError*: HttpErrorCallback
onResponse*: HttpResponseCallback
onUploadProgress*: HttpProgressCallback
onDownloadProgress*: HttpProgressCallback

client: AsyncHttpClient

WebSocketState* = ref object
url*: string
deadline*: float64
closed*: bool

onError*: HttpErrorCallback
onOpen*, onClose*: common.Callback
onMessage*: WebSocketMessageCallback

webSocket: WebSocket

let httpDispatcher = newDispatcher()

var
httpRequests: Table[HttpRequestHandle, HttpRequestState]
webSockets: Table[WebSocketHandle, WebSocketState]

template usingHttpDispatcher(body: untyped) =
let prevDispatcher = getGlobalDispatcher()
setGlobalDispatcher(httpDispatcher)
try:
body
finally:
setGlobalDispatcher(prevDispatcher)

proc newHttpRequestHandle*(): HttpRequestHandle =
let state = HttpRequestState()

while true:
result = windyRand.rand(int.high).HttpRequestHandle
if result notin httpRequests and result.WebSocketHandle notin webSockets:
httpRequests[result] = state
break

proc newWebSocketHandle*(): WebSocketHandle =
let state = WebSocketState()

while true:
result = windyRand.rand(int.high).WebSocketHandle
if result.HttpRequestHandle notin httpRequests and result notin webSockets:
webSockets[result] = state
break

proc getState*(handle: HttpRequestHandle): HttpRequestState =
httpRequests.getOrDefault(handle, nil)

proc getState*(handle: WebSocketHandle): WebSocketState =
webSockets.getOrDefault(handle, nil)

proc cancel*(handle: HttpRequestHandle) =
let state = handle.getState()
if state == nil:
return
state.canceled = true
state.client.close()

proc close*(handle: WebSocketHandle) =
let state = handle.getState()
if state == nil:
return
state.closed = true
if state.webSocket != nil:
try:
state.webSocket.hangup()
except:
discard

proc httpRequestTasklet(handle: HttpRequestHandle) {.async.} =
await sleepAsync(0) # Sleep until next poll

let state = handle.getState()
if state.canceled:
return

state.client = newAsyncHttpClient()

let onProgressChanged = proc(total, progress, speed: BiggestInt) {.async.} =
if state.onDownloadProgress != nil:
let total = if total == 0: -1 else: total.int
state.onDownloadProgress(progress.int, total.int)

state.client.onProgressChanged =
cast[ProgressChangedProc[Future[void]]](onProgressChanged)

let headers = newHttpHeaders()
for header in state.headers:
headers[header.key] = header.value

let httpResponse = HttpResponse()

try:
let response = await state.client.request(
state.url,
state.verb.toUpperAscii(),
state.requestBody,
headers
)

if state.canceled:
handle.cancel()
else:
httpResponse.code = response.code.int
httpResponse.body = await response.body
except:
if not state.canceled and state.onError != nil:
state.onError(getCurrentExceptionMsg())
httpRequests.del(handle)
return

if not state.canceled and state.onDownloadProgress != nil:
state.onDownloadProgress(httpResponse.body.len, httpResponse.body.len)
if not state.canceled and state.onResponse != nil:
state.onResponse(httpResponse)

httpRequests.del(handle)

proc webSocketTasklet(handle: WebSocketHandle) {.async.} =
await sleepAsync(0) # Sleep until next poll

let state = handle.getState()
if state.closed:
return

var skipOnClose: bool
try:
state.webSocket = await newWebSocket(state.url)
if state.closed:
skipOnClose = true
state.webSocket.hangup()
else:
if state.onOpen != nil:
state.onOpen()
while not state.closed:
let (opcode, data) = await state.webSocket.receivePacket()
case opcode:
of Text:
if state.onMessage != nil:
state.onMessage(data, Utf8Message)
of Binary:
if state.onMessage != nil:
state.onMessage(data, BinaryMessage)
of Ping:
await state.webSocket.send(data, Pong)
of Pong:
discard
of Cont:
discard
of Close:
state.webSocket.hangup()
break
except:
if not state.closed and state.onError != nil:
state.onError(getCurrentExceptionMsg())

try:
if state.webSocket.readyState != Closed:
state.webSocket.hangup()
except:
discard

if not skipOnClose and state.onClose != nil:
state.onClose()

webSockets.del(handle)

proc startTasklet*(handle: HttpRequestHandle) =
try:
usingHttpDispatcher:
asyncCheck handle.httpRequestTasklet()
except:
quit("Failed to start HTTP request tasklet")

proc startTasklet*(handle: WebSocketHandle) =
try:
usingHttpDispatcher:
asyncCheck handle.webSocketTasklet()
except:
quit("Failed to start WebSocket tasklet")

proc pollHttp*() =
usingHttpDispatcher:
if hasPendingOperations():
poll(0)

let now = epochTime()

for handle, state in httpRequests:
if state.deadline > 0 and state.deadline <= now:
let msg = "Deadline of " & $state.deadline & " exceeded, time is " & $now
handle.cancel()
if state.onError != nil:
state.onError(msg)

for handle, state in webSockets:
if state.deadline > 0 and state.deadline <= now and state.webSocket == nil:
let msg = "Deadline of " & $state.deadline & " exceeded, time is " & $now
handle.close()
if state.onError != nil:
state.onError(msg)
131 changes: 130 additions & 1 deletion src/windy/platforms/macos/platform.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ../../common, ../../internal, macdefs, opengl, pixie/fileformats/png,
import ../../common, ../../http, ../../internal, macdefs, opengl, pixie/fileformats/png,
pixie/images, times, unicode, utils, vmath

type
Expand Down Expand Up @@ -717,6 +717,8 @@ proc pollEvents*() =
break
NSApp.sendEvent(event)

pollHttp()

proc makeContextCurrent*(window: Window) =
window.inner.contentView.NSOpenGLView.openGLContext.makeCurrentContext()

Expand Down Expand Up @@ -966,3 +968,130 @@ proc getScreens*(): seq[Screen] =
bottom: frame.origin.y.int + frame.size.height.int,
primary: i == 0
))

proc startHttpRequest*(
url: string,
verb = "GET",
headers = newSeq[HttpHeader](),
body = "",
deadline = defaultHttpDeadline
): HttpRequestHandle {.raises: [].} =
init()

result = newHttpRequestHandle()

var headers = headers
headers.addDefaultHeaders()

let state = result.getState()
state.url = url
state.verb = verb
state.headers = headers
state.requestBody = body
state.deadline = deadline

result.startTasklet()

proc deadline*(handle: HttpRequestHandle): float64 =
let state = handle.getState()
if state == nil:
return
state.deadline

proc `deadline=`*(handle: HttpRequestHandle, deadline: float64) =
let state = handle.getState()
if state == nil:
return
state.deadline = deadline

proc `onError=`*(
handle: HttpRequestHandle,
callback: HttpErrorCallback
) =
let state = handle.getState()
if state == nil:
return
state.onError = callback

proc `onResponse=`*(
handle: HttpRequestHandle,
callback: HttpResponseCallback
) =
let state = handle.getState()
if state == nil:
return
state.onResponse = callback

proc `onUploadProgress=`*(
handle: HttpRequestHandle,
callback: HttpProgressCallback
) =
let state = handle.getState()
if state == nil:
return
state.onUploadProgress = callback

proc `onDownloadProgress=`*(
handle: HttpRequestHandle,
callback: HttpProgressCallback
) =
let state = handle.getState()
if state == nil:
return
state.onDownloadProgress = callback

proc cancel*(handle: HttpRequestHandle) =
http.cancel(handle)

proc openWebSocket*(
url: string,
deadline = defaultHttpDeadline
): WebSocketHandle {.raises: [].} =
init()

result = newWebSocketHandle()

let state = result.getState()
state.url = url
state.deadline = deadline

result.startTasklet()

proc `onError=`*(
handle: WebSocketHandle,
callback: HttpErrorCallback
) =
let state = handle.getState()
if state == nil:
return
state.onError = callback

proc `onOpen=`*(
handle: WebSocketHandle,
callback: Callback
) =
let state = handle.getState()
if state == nil:
return
state.onOpen = callback

proc `onMessage=`*(
handle: WebSocketHandle,
callback: WebSocketMessageCallback
) =
let state = handle.getState()
if state == nil:
return
state.onMessage = callback

proc `onClose=`*(
handle: WebSocketHandle,
callback: Callback
) =
let state = handle.getState()
if state == nil:
return
state.onClose = callback

proc close*(handle: WebSocketHandle) =
http.close(handle)
Loading