diff --git a/racketscript-extras/racketscript/htdp/peer-universe.rkt b/racketscript-extras/racketscript/htdp/peer-universe.rkt new file mode 100644 index 00000000..cedf4363 --- /dev/null +++ b/racketscript-extras/racketscript/htdp/peer-universe.rkt @@ -0,0 +1,590 @@ +#lang racketscript/base + +(require (for-syntax racketscript/base + syntax/parse) + "./private/peer-universe/universe-primitives.rkt" + "./private/peer-universe/jscommon.rkt" + "./private/peer-universe/encode-decode.rkt" + "./private/peer-universe/debug-tools.rkt" + "./private/peer-universe/universe-server.rkt") + +(provide on-mouse + on-tick + on-key + on-release + on-receive + register + name + to-draw + stop-when + big-bang + + on-new + on-msg + on-disconnect + server-id + universe + + package? + make-package + + bundle? + make-bundle + mail? + make-mail + + iworld-name + iworld? + iworld=? + + key=? + mouse=?) + +(define *default-frames-per-second* 70) + +(define (make-big-bang init-world handlers dom-root) + (new (BigBang init-world handlers (if ($/binop != dom-root $/null) + dom-root #js*.document.body)))) + +(define (big-bang init-world #:dom-root [dom-root $/null] . handlers) + ($> (make-big-bang init-world handlers dom-root) + (setup) + (start))) + +(define-proto BigBang + (λ (init-world handlers dom-root) + #:with-this this + (:= #js.this.world init-world) + (:= #js.this.interval (/ 1000 *default-frames-per-second*)) + (:= #js.this.handlers handlers) + + (:= #js.this.is-universe? #false) + + (:= #js.this.dom-root dom-root) + + (:= #js.this.-active-handlers ($/obj)) + (:= #js.this.-world-change-listeners ($/array)) + (:= #js.this.-package-listeners ($/array)) + + (:= #js.this.-uses-peer #f) + (:= #js.this.-peer-name #js"client") + (:= #js.this.-server-id #js"server") + (:= #js.this.-peer $/undefined) + (:= #js.this.-conn $/undefined) + (:= #js.this.-peer-init-tasks ($/array)) + + (:= #js.this.-idle #t) + (:= #js.this.-stopped #t) + (:= #js.this.-events ($/array)) + + (define canvas (#js.document.createElement #js"canvas")) + (define ctx (#js.canvas.getContext #js"2d")) + (#js.canvas.setAttribute #js"tabindex" 1) + (#js.canvas.setAttribute #js"style" #js"outline: none") + (:= #js.this.-canvas canvas) + (:= #js.this.-context ctx)) + [setup + (λ () + #:with-this this + + (define canvas #js.this.-canvas) + + (#js.this.dom-root.appendChild canvas) + (#js.canvas.focus) + + (#js.this.register-handlers) + + (if #js.this.-uses-peer + (#js.this.init-peer-connection) + (void)) + + ;; Set canvas size as the size of first world + (define draw-handler ($ #js.this.-active-handlers #js"to-draw")) + (unless draw-handler + (error 'big-bang "to-draw handle not provided")) + (define img ($$ draw-handler.callback #js.this.world)) + (:= #js.canvas.width #js.img.width) + (:= #js.canvas.height #js.img.height) + + ;; We are reassigning using change-world so that change world + ;; callbacks gets invoked at start of big-bang + (#js.this.change-world #js.this.world) + + this)] + [register-handlers + (λ () + #:with-this this + (define active-handlers #js.this.-active-handlers) + (let loop ([handlers #js.this.handlers]) + (when (pair? handlers) + (define h ((car handlers) this)) + (#js.h.register) + (:= ($ active-handlers #js.h.name) h) + (loop (cdr handlers)))))] + [deregister-handlers + (λ () + #:with-this this + (define active-handlers #js.this.-active-handlers) + ($> (#js*.Object.keys active-handlers) + (forEach + (λ (key) + (define h ($ active-handlers key)) + (#js.h.deregister) + (:= ($ #js.active-handlers #js.h.name) *undefined*)))))] + [start + (λ () + #:with-this this + (:= #js.this.-stopped #f) + ; always draw first, in case no on-tick handler provided + (#js.this.queue-event ($/obj [type #js"to-draw"])) + (#js.this.process-events))] + [stop + (λ () + #:with-this this + (#js.this.clear-event-queue) + (set-object! this + [-stopped #t] + [-idle #t]) + (#js.this.deregister-handlers) + (#js.this.-canvas.remove) + (set-object! #js.this + [-active-handlers ($/obj)] + [handlers '()]))] + [clear-event-queue + (λ () + #:with-this this + (#js.this.-events.splice 0 #js.this.-events.length))] + [queue-event + (λ (e) + #:with-this this + (#js.this.-events.push e) + (when #js.this.-idle + (schedule-animation-frame #js.this 'process_events)))] + [change-world + (λ (handler-result) + #:with-this this + + ;; WIP: handle packages being passed as new-world + ;; see https://docs.racket-lang.org/teachpack/2htdpuniverse.html#%28part._universe._.Sending_.Messages%29 + (define new-world handler-result) + (if (package? handler-result) + (begin + (set! new-world (package-world handler-result)) + (#js.this.handle-package handler-result)) + (void)) + + (define listeners #js.this.-world-change-listeners) + (let loop ([i 0]) + (when (< i #js.listeners.length) + (define listener ($ #js.listeners i)) + (listener new-world) + (loop (add1 i)))) + (:= #js.this.world new-world))] + [add-world-change-listener + (λ (cb) + #:with-this this + (#js.this.-world-change-listeners.push cb))] + [remove-world-change-listener + (λ (cb) + #:with-this this + (define index (#js.this.-world-change-listeners.indexOf cb)) + (#js.this.-world-change-listeners.splice index 1))] + [handle-package + (λ (pkg) + #:with-this this + (define message (package-message pkg)) + (define listeners #js.this.-package-listeners) + (let loop ([i 0]) + (when (< i #js.listeners.length) + (define listener ($ #js.listeners i)) + (listener message) + (loop (add1 i)))))] + [add-package-listener + (λ (cb) + #:with-this this + (#js.this.-package-listeners.push cb))] + [remove-package-listener + (λ (cb) + #:with-this this + (define index (#js.this.-package-listeners.indexOf cb)) + (#js.this.-package-listeners.splice index 1))] + [process-events + (λ () + #:with-this this + (define events #js.this.-events) + + (:= #js.this.-idle #f) + + (let loop ([world-changed? #f]) + (cond + [(> #js.events.length 0) + (define evt (#js.events.shift)) + (define handler ($ #js.this.-active-handlers #js.evt.type)) + + (define changed? + (cond + ; raw evt must be checked 1st; bc handler will be undefined + [(equal? #js.evt.type #js"raw") + (#js.evt.invoke #js.this.world evt)] + [($/binop === handler $/undefined) + (begin (#js*.console.warn #js"WARNING: processing event w/ undefined handler.") (void))] + [handler (#js.handler.invoke #js.this.world evt)] + [else + (#js.console.warn "ignoring unknown/unregistered event type: " evt)])) + (loop (or world-changed? changed?))] + [(and world-changed? (not #js.this.-stopped)) + (#js.this.queue-event ($/obj [type #js"to-draw"])) + (loop #f)])) + + (:= #js.this.-idle #t))] + [init-peer-connection + ; Should we let users pick their own IDs? Would that be a security issue? + (λ () + #:with-this this + (define peer (new (Peer))) + (:= #js.this.-peer peer) + + (#js.peer.on #js"open" + (λ () + (define conn (#js.peer.connect (js-string #js.this.-server-id) + ($/obj [label #js.this.-peer-name]))) + (:= #js.this.-conn conn) + (define init-tasks #js.this.-peer-init-tasks) + + (define (on-conn-open) + ;; Loop through this.-peer-init-tasks[] and execute all callbacks + (let loop ([i 0]) + (when (< i #js.init-tasks.length) + (define task ($ #js.init-tasks i)) + (task peer conn) + (loop (add1 i)))) + ;; Add beforeunload and unload listeners to close the connection + (#js*.window.addEventListener #js"beforeunload" + (λ (_) + (#js.conn.close))) + (#js*.window.addEventListener #js"unload" + (λ (_) + (#js.conn.close) + )) + ) + (#js.conn.on #js"open" on-conn-open) + (#js.conn.on #js"close" (λ (_) ( + ;; TODO: implement disconnect event + #js*.console.log #js"conn closed") + (#js*.alert #js"Client has been disconnected by the server or the connection has been lost."))) + )))] + ;; cb = (peer: Peer, conn: DataConnection) => void + [add-peer-init-task + (λ (cb) + #:with-this this + ;; If peer and conn already exist, execute callback + ;; else, append callback to this.-peer-init-tasks[] + (define conn #js.this.-conn) + (define peer #js.this.-peer) + (define conn-open? + (if ($/typeof conn "undefined") + #f #js.conn.open)) + (if conn-open? + (cb peer conn) + (#js.this.-peer-init-tasks.push cb)))]) + +(define (to-draw cb) + (λ (bb) + (define on-tick-evt ($/obj [type #js"to-draw"])) + ($/obj + [name #js"to-draw"] + [register (λ () (void))] + [deregister (λ () (void))] + [callback cb] + [invoke (λ (world evt) + (define ctx #js.bb.-context) + (define img (cb #js.bb.world)) + (define height #js.img.height) + (define width #js.img.width) + + (#js.ctx.clearRect 0 0 width height) + (#js.img.render ctx (half width) (half height)) + + #f)]))) + +(define (on-tick cb rate) + (λ (bb-u) + (define on-tick-evt ($/obj [type #js"on-tick"])) + ($/obj + [name #js"on-tick"] + [register (λ () + #:with-this this + (#js.bb-u.queue-event on-tick-evt) + (if rate + (set! rate (* 1000 rate)) + (set! rate #js.bb-u.interval)))] + [deregister (λ () + #:with-this this + (define last-cb #js.this.last-cb) + (when last-cb + ;; TODO: This sometimes doesn't work, + ;; particularly with high fps, so we need to do + ;; something at event loop itself. + (#js*.window.clearTimeout last-cb)))] + [invoke (λ (state _) + #:with-this this + (if #js.bb-u.is-universe? + (#js.bb-u.change-state (cb state)) + (#js.bb-u.change-world (cb state))) + (:= #js.this.last-cb (#js*.setTimeout + (λ () + (#js.bb-u.queue-event on-tick-evt)) + rate)) + #t)]))) + +(define (on-mouse cb) + (λ (bb) + ($/obj + [name #js"on-mouse"] + [listeners ($/obj)] + [register + (λ () + #:with-this this + (define canvas #js.bb.-canvas) + (define (make-listener r-evt-name) + (λ (evt) + (define posn (canvas-posn-δ canvas evt)) + (#js.bb.queue-event ($/obj [type #js"on-mouse"] + [evt (js-string->string r-evt-name)] + [x ($ posn 'x)] + [y ($ posn 'y)])))) + + (define (register-listener evt-name r-evt-name) + (define cb (make-listener r-evt-name)) + (#js.canvas.addEventListener evt-name cb) + (:= ($ #js.this.listeners evt-name) cb)) + + (register-listener #js"mousemove" #js"move") + (register-listener #js"mousedown" #js"button-down") + (register-listener #js"mouseup" #js"button-up") + (register-listener #js"mouseout" #js"leave") + (register-listener #js"mouseover" #js"enter") + (register-listener #js"drag" #js"drag"))] + [deregister + (λ () + #:with-this this + (define (remove-listener evt-name) + (define cb ($ #js.this.listeners evt-name)) + (#js.bb.-canvas.removeEventListener evt-name cb)) + (remove-listener #js"mousemove") + (remove-listener #js"mousedown") + (remove-listener #js"mouseup") + (remove-listener #js"mouseout") + (remove-listener #js"mouseover") + (remove-listener #js"drag"))] + [invoke + (λ (world evt) + (define new-world (cb world #js.evt.x #js.evt.y #js.evt.evt)) + (#js.bb.change-world new-world) + #t)]))) + +(define-syntax-rule (-on-key-* r-evt-name evt-name) + (λ (cb) + (λ (bb) + ($/obj + [name r-evt-name] + [register + (λ () + #:with-this this + (define canvas #js.bb.-canvas) + (:= #js.this.listener + (λ (evt) + (#js.evt.preventDefault) + (#js.evt.stopPropagation) + (#js.bb.queue-event ($/obj [type r-evt-name] + [key (key-event->key-name evt)])))) + (#js.canvas.addEventListener evt-name #js.this.listener))] + [deregister + (λ () + #:with-this this + (#js.bb.-canvas.removeEventListener evt-name #js.this.listener) + (:= #js.this.listener *undefined*))] + [invoke + (λ (world evt) + (define new-world (cb world #js.evt.key)) + (#js.bb.change-world new-world) + #t)])))) + +(define on-key (-on-key-* #js"on-key" #js"keydown")) +(define on-release (-on-key-* #js"on-release" #js"keyup")) + +(define (stop-when last-world? [last-picture #f]) + (λ (bb) + ($/obj + [name #js"stop-when"] + [predicate last-world?] + [lastpicture last-picture] + [register + (λ () + #:with-this this + (#js.bb.add-world-change-listener #js.this.invoke))] + [deregister + (λ () + #:with-this this + (#js.bb.remove-world-change-listener #js.this.invoke))] + [invoke + (λ (w) + (when (last-world? w) + (#js.bb.stop) + (when last-picture + (define handler ((to-draw last-picture) bb)) + (#js.bb.queue-event + ($/obj [type #js"raw"] + [invoke #js.handler.invoke])))))]))) + +;; maps JS KeyboardEvent.key to big-bang KeyEvent +(define key-table + ($/obj [Backspace "\b"] + [Enter "\r"] + [Tab "\t"] + [ArrowLeft "left"] + [ArrowRight "right"] + [ArrowDown "down"] + [ArrowUp "up"] + [Shift "shift"] + [Control "control"] + [ControlRight "rcontrol"] + [ControlLeft "control"] + [ShiftRight "rshift"] + [ShiftLeft "shift"] + [Escape "escape"] + [Home "home"] + [End "end"] + [Insert "insert"] ; no pageup/down in big-bang? + [Delete "\u007F"] ; rubout + [Pause "pause"] + [NumLock "numlock"] + [F1 "f1"] + [F2 "f2"] + [F3 "f3"] + [F4 "f4"] + [F5 "f5"] + [F6 "f6"] + [F7 "f7"] + [F8 "f8"] + [F9 "f9"] + [F10 "f10"] + [F11 "f11"] + [F12 "f12"] + ; unsure about these big bang KeyEvents: + ;; "start" + ;; "cancel" + ;; "clear" + ;; "menu" + ;; "capital" + ;; "prior" + ;; "next" + ;; "select" + ;; "print" + ;; "execute" + ;; "snapshot" + ;; "help" + ;; "scroll" + )) + +(define (key-event->key-name e) + (define k #js.e.key) + (define code ; use .code to differentiate left/right shift, ctrl, alt + (if (or ($/binop === k #js"Shift") ($/binop === k #js"Control") ($/binop === k #js"Alt")) + #js.e.code + k)) + (let ([key-table-code ($ key-table code)]) + (if (void? key-table-code) + (js-string->string code) + key-table-code))) + +(define (canvas-posn-δ canvas evt) + (define rect (#js.canvas.getBoundingClientRect)) + ($/obj + [x (- #js.evt.clientX #js.rect.left)] + [y (- #js.evt.clientY #js.rect.top)])) + +(define (key=? k1 k2) + (equal? k1 k2)) +(define (mouse=? m1 m2) + (equal? m1 m2)) + +(define (on-receive cb) + (λ (bb) + (define on-receive-evt ($/obj [type #js"on-receive"])) + ($/obj + [name #js"on-receive"] + [register (λ () + #:with-this this + + (#js.bb.add-peer-init-task + (λ (peer conn) + (:= #js.this.conn-data-listener + (λ (data) + (#js.bb.queue-event ($/obj [type #js.on-receive-evt.type] + [msg data])))) + + (#js.conn.on #js"data" #js.this.conn-data-listener) + + (:= #js.this.package-listener + (λ (message) + #:with-this this + (#js.conn.send (encode-data message)) + 0)) + + (#js.bb.add-package-listener #js.this.package-listener))) + + 0)] + [deregister (λ () + #:with-this this + (define peer #js.bb.-peer) + (define should-destroy-peer? + (if ($/typeof peer "undefined") + #f + (not #js.peer.disconnected))) + (if should-destroy-peer? + (begin + (#js.peer.disconnect) + (#js.peer.destroy)) + (void)) + (#js.bb.remove-package-listener #js.this.package-listener) + 0)] + [invoke (λ (world evt) + #:with-this this + (#js.bb.change-world (cb world (decode-data #js.evt.msg))) + #t)]))) + +(define (register server-id) + (λ (bb) + ($/obj + [name #js"register"] + [register (λ () + #:with-this this + (:= #js.bb.-server-id server-id) + (:= #js.bb.-uses-peer #t) + 0)] + [deregister (λ () + #:with-this this + (define conn #js.bb.-conn) + (define conn-open? + (if ($/typeof conn "undefined") + #f #js.conn.open)) + (#js*.console.log conn-open?) + (if conn-open? + (#js.conn.close) + (void)) + 0)] + [invoke (λ (world evt) + #:with-this this + #t + )]))) + +(define (name name) + (λ (bb) + ($/obj + [name #js"name"] + [register (λ () + #:with-this this + (:= #js.bb.-peer-name (js-string name)) + (void))] + [deregister (λ () (void))]))) \ No newline at end of file diff --git a/racketscript-extras/racketscript/htdp/private/peer-universe/debug-tools.rkt b/racketscript-extras/racketscript/htdp/private/peer-universe/debug-tools.rkt new file mode 100644 index 00000000..012dcc4f --- /dev/null +++ b/racketscript-extras/racketscript/htdp/private/peer-universe/debug-tools.rkt @@ -0,0 +1,20 @@ +#lang racketscript/base + +(require "encode-decode.rkt") + +(provide console-log-rkt-list + test-encoding) + +(define (console-log-rkt-list l) + (if (list? l) (#js*.console.log (foldl (lambda (curr res) + (#js.res.push curr) + res) + ($/array) l)) + (#js*.console.log l))) + +(define (test-encoding val) + (define result (decode-data (encode-data val))) + (#js*.console.log val) + (#js*.console.log result) + (#js*.console.log (js-string (format "val == result? : ~a" (equal? val result)))) + (void)) \ No newline at end of file diff --git a/racketscript-extras/racketscript/htdp/private/peer-universe/encode-decode.rkt b/racketscript-extras/racketscript/htdp/private/peer-universe/encode-decode.rkt new file mode 100644 index 00000000..a24c039c --- /dev/null +++ b/racketscript-extras/racketscript/htdp/private/peer-universe/encode-decode.rkt @@ -0,0 +1,88 @@ +#lang racketscript/base + +(provide encode-data + decode-data) + +(require "util.rkt") + +(define DATA-TYPE-WARNING #js"racketscript/htdp/universe: Unsupported datatype being passed to/from server.") + +(define (encode-array arr) + (#js.arr.map (lambda (elem) (encode-data elem)))) + +(define (decode-array arr) + (#js.arr.map (lambda (elem) (decode-data elem)))) + +(define (encode-object obj) + (define keys (#js*.Object.keys obj)) + (#js.keys.reduce (lambda (res key) + ($/:= ($ res key) (encode-data ($ obj key))) + res) + ($/obj))) + +(define (decode-object obj) + (define keys (#js*.Object.keys obj)) + (#js.keys.reduce (lambda (res key) + ($/:= ($ res key) (decode-data ($ obj key))) + res) + ($/obj))) + +#| +('test "some_string" #js"test" {test: "test"}) + + +"test" +{ + val: "test", type: "string" +} + +'sym +{ + val: "sym", type: "symbol" +} + +|# + +(define (encode-data data) + (cond [(list? data) (foldl (lambda (curr result) + (#js.result.push (encode-data curr)) + result) + ($/array) + data)] + [(null? data) ($/obj [type #js"null"])] + [(undefined? data) ($/obj [type #js"undefined"])] + [(number? data) ($/obj [type #js"number"] + [val data])] + [(string? data) ($/obj [type #js"string"] + [val (js-string data)])] + [(symbol? data) ($/obj [type #js"symbol"] + [val (js-string (symbol->string data))])] + [(boolean? data) ($/obj [type #js"boolean"] + [val data])] + [(js-string? data) ($/obj [type #js"js-string"] + [val data])] + [(js-array? data) ($/obj [type #js"js-array"] + [val (encode-array data)])] + [(js-object? data) ($/obj [type #js"js-object"] + [val (encode-object data)])] + [else (begin + (#js*.console.warn ($/array DATA-TYPE-WARNING data)) + ($/obj [type #js"unknown"] + [val data]))])) + +(define (decode-data data) + (cond [(#js*.Array.isArray data) (#js.data.reduce (lambda (result curr) + (append result (list (decode-data curr)))) + '())] + [($/binop == #js.data.type #js"null") $/null] + [($/binop == #js.data.type #js"undefined") $/undefined] + [($/binop == #js.data.type #js"number") #js.data.val] + [($/binop == #js.data.type #js"string") (js-string->string #js.data.val)] + [($/binop == #js.data.type #js"symbol") (string->symbol (js-string->string #js.data.val))] + [($/binop == #js.data.type #js"boolean") #js.data.val] + [($/binop == #js.data.type #js"js-string") #js.data.val] + [($/binop == #js.data.type #js"js-array") (decode-array #js.data.val)] + [($/binop == #js.data.type #js"js-object") (decode-object #js.data.val)] + [($/binop == #js.data.type #js"unknown") (begin + (#js*.console.warn DATA-TYPE-WARNING) + #js.data.val)])) \ No newline at end of file diff --git a/racketscript-extras/racketscript/htdp/private/peer-universe/jscommon.rkt b/racketscript-extras/racketscript/htdp/private/peer-universe/jscommon.rkt new file mode 100644 index 00000000..9fad8741 --- /dev/null +++ b/racketscript-extras/racketscript/htdp/private/peer-universe/jscommon.rkt @@ -0,0 +1,91 @@ +#lang racketscript/base + +(require (for-syntax racketscript/base + syntax/parse)) + +(provide := + *this* + *null* + *undefined* + new + define-proto + set-object! + schedule-method + schedule-animation-frame + document + console + Math + Path2D + abs + sin + cos + floor + abs+ceil + max + min + twice + half + (rename-out [field-λ λ])) + +;;----------------------------------------------------------------------------- +;; Interop helpers + +(define-syntax := (make-rename-transformer #'$/:=)) +(define-syntax new (make-rename-transformer #'$/new)) +(define-syntax *this* (make-rename-transformer #'$/this)) +(define-syntax *null* (make-rename-transformer #'$/null)) +(define-syntax *undefined* (make-rename-transformer #'$/undefined)) + +(begin-for-syntax + (define-syntax-class field + #:description "a key-value pair for object" + (pattern [name:id val:expr]))) + +(define-syntax (field-λ stx) + (syntax-parse stx + [(_ formals (~datum #:with-this) self:id body ...) + #'(λ formals + (define self *this*) + body ...)] + [(_ formals body ...) #'(λ formals body ...)])) + +(define-syntax (define-proto stx) + (syntax-parse stx + [(define-proto name:id init:expr field:field ...) + #`(begin + (define name init) + #,(when (attribute field) + #`(begin + (:= ($ name 'prototype 'field.name) field.val) ...)))])) + +(define-syntax (set-object! stx) + (syntax-parse stx + [(set-object! obj:expr f:field ...) + #`(begin (:= ($ obj 'f.name) f.val) ...)])) + + +(define-syntax-rule (schedule-method this method interval) + (let ([self this]) + (#js*.window.setTimeout (λ () + (($ self method))) + interval))) + +(define-syntax-rule (schedule-animation-frame this step) + (let ([self this]) + (#js*.window.requestAnimationFrame (λ () + (($ self step)))))) + +;;----------------------------------------------------------------------------- +;; Helper functions + +(define document #js*.window.document) +(define console #js*.window.console) +(define Math #js*.window.Math) +(define Path2D #js*.window.Path2D) +(define abs+ceil (λ (n) (abs (ceiling n)))) + +(define-syntax-rule (twice e) + (* e 2)) + +(define-syntax-rule (half e) + (/ e 2)) \ No newline at end of file diff --git a/racketscript-extras/racketscript/htdp/private/peer-universe/server-gui.rkt b/racketscript-extras/racketscript/htdp/private/peer-universe/server-gui.rkt new file mode 100644 index 00000000..9decba16 --- /dev/null +++ b/racketscript-extras/racketscript/htdp/private/peer-universe/server-gui.rkt @@ -0,0 +1,115 @@ +#lang racketscript/base + +(require (for-syntax racketscript/base + syntax/parse) + "encode-decode.rkt" + "debug-tools.rkt" + "universe-primitives.rkt" + "jscommon.rkt") + +(provide server-gui) + +(define DEFAULT-DISPLAY-MODE #js"block") +(define WIDTH 500) +(define HEIGHT 300) + +(define-proto ServerLogger + (λ (root stop-callback restart-callback) + #:with-this this + + ;
+ ; Auto-scroll + ; logged text + ;
+ ; + ; + ;
+ ;
+ (:= #js.this.logs ($/array)) + (:= #js.this.autoscroll? #true) + + ;; Create elements + (:= #js.this.container (#js*.document.createElement #js"div")) + (:= #js.this.textbox (#js*.document.createElement #js"textarea")) + (:= #js.this.checkbox-div (#js*.document.createElement #js"div")) + (:= #js.this.checkbox-label (#js*.document.createElement #js"label")) + (:= #js.this.checkbox (#js*.document.createElement #js"input")) + (:= #js.this.button-div (#js*.document.createElement #js"div")) + (:= #js.this.stop-button (#js*.document.createElement #js"button")) + (:= #js.this.restart-button (#js*.document.createElement #js"button")) + + ;; Configure elements + (:= #js.this.container.style.display #js"none") + (:= #js.this.container.style.width (js-string (format "~apx" WIDTH))) + (:= #js.this.container.style.height (js-string (format "~apx" HEIGHT))) + + (:= #js.this.textbox.style.width #js"inherit") + (:= #js.this.textbox.style.height #js"inherit") + + (:= #js.this.checkbox-label.for #js"autoscroll") + (:= #js.this.checkbox-label.innerHTML #js"autoscroll with new input") + (:= #js.this.checkbox.type #js"checkbox") + (:= #js.this.checkbox.onclick (lambda () (:= #js.this.autoscroll? #js.this.checkbox.checked))) + (:= #js.this.checkbox.checked #true) + + (:= #js.this.stop-button.innerHTML #js"stop") + (:= #js.this.stop-button.style.grid-area #js"stop") + (:= #js.this.stop-button.onclick stop-callback) + (:= #js.this.restart-button.innerHTML #js"restart") + (:= #js.this.restart-button.style.grid-area #js"restart") + (:= #js.this.restart-button.onclick restart-callback) + (:= #js.this.button-div.style.width #js"100%") + (:= #js.this.button-div.style.display #js"grid") + (:= #js.this.button-div.style.gridTemplateAreas + #js"'stop restart'") + + ;; Add elements to document + (#js.this.checkbox-div.appendChild #js.this.checkbox-label) + (#js.this.checkbox-div.appendChild #js.this.checkbox) + + (#js.this.button-div.appendChild #js.this.stop-button) + (#js.this.button-div.appendChild #js.this.restart-button) + + (#js.this.container.appendChild #js.this.textbox) + (#js.this.container.appendChild #js.this.checkbox-div) + (if (and restart-callback stop-callback) + (#js.this.container.appendChild #js.this.button-div) + (void)) + (#js.root.appendChild #js.this.container) + this) + [log + (λ (text) + #:with-this this + (#js.this.logs.push (js-string text)) + (#js.this.render) + (#js*.console.log (js-string text)) + (void))] + [show + (λ () + #:with-this this + (:= #js.this.container.style.display DEFAULT-DISPLAY-MODE) + (void))] + [hide + (λ () + #:with-this this + (:= #js.this.container.style.display #js"none") + (void))] + [render + (λ () + #:with-this this + (define log-string (#js.this.logs.reduce (λ (res curr) + (if ($/binop === res #js"") + (js-string curr) + ($/+ res #js"\n\n" (js-string curr)))) + #js"")) + (:= #js.this.textbox.innerHTML log-string) + (cond [(equal? #js.this.autoscroll? #true) + (:= #js.this.textbox.scrollTop #js.this.textbox.scrollHeight)] + [else (void)]) + (void))]) + +(define (make-gui root stop-callback restart-callback) + (new (ServerLogger root stop-callback restart-callback))) + +(define (server-gui [root-element #js*.document.body] [stop-callback #false] [restart-callback #false]) + (make-gui root-element stop-callback restart-callback)) \ No newline at end of file diff --git a/racketscript-extras/racketscript/htdp/private/peer-universe/universe-primitives.rkt b/racketscript-extras/racketscript/htdp/private/peer-universe/universe-primitives.rkt new file mode 100644 index 00000000..0103b7eb --- /dev/null +++ b/racketscript-extras/racketscript/htdp/private/peer-universe/universe-primitives.rkt @@ -0,0 +1,95 @@ +#lang racketscript/base + +(require ;htdp/error + racket/list) + +(provide sexp? + + make-package + package? + package-world + package-message + + make-bundle + bundle? + + make-mail + mail? + + iworld-name + iworld? + iworld=? + + ;; private + bundle-state + bundle-mails + bundle-low-to-remove + + ;; private + mail-to + mail-content + + ;; private + make-iworld + iworld-conn) + +(define (sexp? x) + (cond + [(empty? x) #true] + [(string? x) #true] + [(bytes? x) #true] + [(symbol? x) #true] + [(number? x) #true] + [(boolean? x) #true] + [(char? x) #true] + [(pair? x) (and (list? x) (andmap sexp? x))] + ; [(and (struct? x) (prefab-struct-key x)) (for/and ((i (struct->vector x))) (sexp? i))] + [else #false])) + +(struct u-package (world message)) +(define (make-package world message) + (u-package world message)) +(define (package? p) + (u-package? p)) +(define (package-world p) + (u-package-world p)) +(define (package-message p) + (u-package-message p)) + +(struct u-bundle (state mails low-to-remove)) +(define (make-bundle state mails low-to-remove) + (u-bundle state mails low-to-remove)) +(define (bundle? bundle) + (u-bundle? bundle)) +(define (bundle-state b) + (u-bundle-state b)) +(define (bundle-mails b) + (u-bundle-mails b)) +(define (bundle-low-to-remove b) + (u-bundle-low-to-remove b)) + +(struct u-mail (to content)) +(define (make-mail to content) + (u-mail to content)) +(define (mail? mail) + (u-mail? mail)) +(define (mail-to mail) + (u-mail-to mail)) +(define (mail-content mail) + (u-mail-content mail)) + +(struct u-iworld (conn name)) +;; for client code use +(define (iworld-name iworld) + (u-iworld-name iworld)) +(define (iworld? iworld) + (u-iworld? iworld)) +(define (iworld=? iw1 iw2) + (define conn1 (u-iworld-conn iw1)) + (define conn2 (u-iworld-conn iw2)) + ($/binop === conn1 conn2)) +;; not for client code use +(define (make-iworld conn name) + (u-iworld conn name)) +(define (iworld-conn iw) + (u-iworld-conn iw)) \ No newline at end of file diff --git a/racketscript-extras/racketscript/htdp/private/peer-universe/universe-server.rkt b/racketscript-extras/racketscript/htdp/private/peer-universe/universe-server.rkt new file mode 100644 index 00000000..bfcc7144 --- /dev/null +++ b/racketscript-extras/racketscript/htdp/private/peer-universe/universe-server.rkt @@ -0,0 +1,345 @@ +#lang racketscript/base + +(require (for-syntax racketscript/base + syntax/parse) + "server-gui.rkt" + "encode-decode.rkt" + "debug-tools.rkt" + "universe-primitives.rkt" + "jscommon.rkt" + "util.rkt") + +; TODO: +; implement deregister for on-msg handler +; implement the following handlers +; - to-string +; - check-with +; - state + +; Variations from api: +; - no port handler +; - create clause for user to pass in +; root element for logging GUI + +; Add to logs: +; u: current universe state +; Events to log: +; - mail sending: +; "broadcast failed to ~a" iworld name +; "~s not on the list" iworld name + +(provide universe + + on-new + on-msg + on-disconnect + server-id + + ; peerjs + Peer) + +;; Adds peerjs exports (primarily Peer constructor) to window object +(define peerjs ($/require "https://cdnjs.cloudflare.com/ajax/libs/peerjs/1.4.7/peerjs.min.js" *)) + +(define Peer #js*.window.Peer) + +(define DEFAULT-UNIVERSE-ID "server") ;; Change this + +(define *default-frames-per-second* 70) + +;; Universe server +(define (make-universe init-state handlers gui-root) + (new (Universe init-state handlers (if ($/binop != gui-root $/null) + gui-root #js*.document.body)))) + +(define (universe init-state #:dom-root [gui-root $/null] . handlers) + ($> (make-universe init-state handlers gui-root) + (setup) + (start))) + +(define-proto Universe + (λ (init-state handlers gui-root) + #:with-this this + (:= #js.this.state init-state) + (:= #js.this.interval (/ 1000 *default-frames-per-second*)) + (:= #js.this.handlers handlers) + + (:= #js.this.is-universe? #true) + + (:= #js.this.gui (server-gui gui-root + ; #js.this.stop + ; (λ () ($> #js.this.stop #js.this.setup #js.this.start)) + )) ;; TODO: allow user to pass root element? & Fix stop/restart cb's + + (:= #js.this.-active-handlers ($/obj)) + (:= #js.this.-state-change-listeners ($/array)) + (:= #js.this.-message-listeners ($/array)) + + (:= #js.this.-peer $/undefined) + (:= #js.this.-peer-init-tasks ($/array)) + (:= #js.this.-active-iworlds ($/array)) + (:= #js.this.-disconnect-tasks ($/array)) + + (:= #js.this.-peer-id DEFAULT-UNIVERSE-ID) + + (:= #js.this.-idle #t) + (:= #js.this.-stopped #t) + (:= #js.this.-events ($/array))) + [setup + (λ () + #:with-this this + (#js.this.register-handlers) + (#js.this.gui.show) + + (define (log-connection conn) + (#js.this.gui.log (format "~a signed up" (js-string->string #js.conn.label)))) + (define (log-new-msg iw data) + (#js.this.gui.log (format "~a --> universe:\n<~a>" + (iworld-name iw) (msg->string (decode-data data))))) + + (#js.this.add-peer-init-task (λ (peer) + (#js.peer.on #js"connection" + log-connection))) + (#js.this.-message-listeners.push log-new-msg) + this)] + [start + (λ () + #:with-this this + (#js.this.init-peer-connection) + (#js.this.gui.log (format "a new universe is up and running with id ~s" + (js-string->string #js.this.-peer.id))) + this)] + [register-handlers + (λ () + #:with-this this + (define active-handlers #js.this.-active-handlers) + (let loop ([handlers #js.this.handlers]) + (when (pair? handlers) + (define h ((car handlers) this)) + (#js.h.register) + (:= ($ active-handlers #js.h.name) h) + (loop (cdr handlers)))))] + [deregister-handlers + (λ () + #:with-this this + (define active-handlers #js.this.-active-handlers) + ($> (#js*.Object.keys active-handlers) + (forEach + (λ (key) + (define h ($ active-handlers key)) + (#js.h.deregister) + (:= ($ #js.active-handlers #js.h.name) *undefined*)))))] + [stop + (λ () + #:with-this this + (#js.this.gui.log "stopping the universe\n----------------------------------") + (void))] + [clear-event-queue + (λ () + #:with-this this + (#js.this.-events.splice 0 #js.this.-events.length))] + [add-state-change-listener + (λ () 0)] + [remove-state-change-listener + (λ () 0)] + [queue-event + (λ (e) + #:with-this this + (#js.this.-events.push e) + (when #js.this.-idle + (schedule-animation-frame #js.this 'process_events)))] + [process-events + (λ () + #:with-this this + (define events #js.this.-events) + + (:= #js.this.-idle #f) + + (let loop ([state-changed? #f]) + (cond + [(> #js.events.length 0) + (define evt (#js.events.shift)) + (define handler ($ #js.this.-active-handlers #js.evt.type)) + (define changed? + (cond + ; raw evt must be checked 1st; bc handler will be undefined + [(equal? #js.evt.type #js"raw") + (#js.evt.invoke #js.this.state evt)] + [(not ($/typeof handler "undefined")) + (#js.handler.invoke #js.this.state evt)] + [else + (#js.console.warn "ignoring unknown/unregistered event type: " evt)])) + (loop (or state-changed? changed?))])) + + (:= #js.this.-idle #t))] + [change-state + (λ (result-bundle) + #:with-this this + + (define new-state (bundle-state result-bundle)) + (define mails (bundle-mails result-bundle)) + (define low-to-remove (bundle-low-to-remove result-bundle)) + + ;; Send all mails + (for-each (lambda (curr-mail) + (define iworld (mail-to curr-mail)) + (define conn (iworld-conn iworld)) + (#js.conn.send (encode-data (mail-content curr-mail))) + (#js.this.gui.log (format "universe --> ~a:\n<~a>" + (iworld-name iworld) + (mail-content curr-mail)))) + mails) + + ;; Remove all worlds in low-to-remove + (for-each (lambda (iw) + (define conn (iworld-conn iw)) + (define index (#js.this.-active-iworlds.indexOf iw)) + (#js.conn.close) + (if (> index -1) + (#js.this.-active-iworlds.splice index 1) + (void))) + low-to-remove) + + (define listeners #js.this.-state-change-listeners) + (let loop ([i 0]) + (when (< i #js.listeners.length) + (define listener ($ #js.listeners i)) + (listener new-state) + (loop (add1 i)))) + (:= #js.this.state new-state) + ; (#js.this.gui.log (format "~a" new-state)) + ;; Maybe implement this? + )] + [init-peer-connection + (λ (id) + #:with-this this + (define peer (new (Peer #js.this.-peer-id))) + (:= #js.this.-peer peer) + (#js.peer.on #js"open" + (λ () + (define init-tasks #js.this.-peer-init-tasks) + (let loop ([i 0]) + (when (< i #js.init-tasks.length) + (define task ($ #js.init-tasks i)) + (task peer) + (loop (add1 i)))))))] + [add-peer-init-task + (λ (cb) ;; cb = (peer: Peer) => void + #:with-this this + ;; If peer already exists, execute callback + ;; else, append callback to this.-peer-init-tasks[] + (define peer #js.this.-peer) + (define peer-started? (not ($/typeof peer "undefined"))) + + (if peer-started? + (cb peer) + (#js.this.-peer-init-tasks.push cb)))] + [pass-message ;; Passes sender iworld and message to this.-message-listeners + (λ (sender-iw data) + #:with-this this + ;; TODO: Decrypt data once encryption/decryption of racket types solved + (#js.this.-message-listeners.forEach + (λ (cb) (cb sender-iw data))))] + [handle-disconnect + (λ (iw) + #:with-this this + ;; Run all disconnect tasks, passing in the iworld of the connection being closed + (define tasks #js.this.-disconnect-tasks) + (let loop ([i 0]) + (when (< i #js.tasks.length) + (define task ($ tasks i)) + (task iw) + (loop (add1 i)))) + (#js.this.gui.log (format "~a !! closed port" (iworld-name iw))) + (void))]) + +(define (on-new cb) + (λ (u) + (define on-new-evt ($/obj [type #js"on-new"])) + ($/obj + [name #js"on-new"] + [register (λ () + #:with-this this + (define (init-task peer) + (define (handle-connection conn) + (define name "client name") + (if #js.conn.label + (set! name (js-string->string #js.conn.label)) + (void)) + (define iw (make-iworld conn name)) + (#js.u.-active-iworlds.push iw) + (#js.u.queue-event ($/obj [type #js"on-new"] + [iWorld iw])) + (#js.conn.on #js"close" + (λ () + (#js.u.handle-disconnect iw))) + (#js.conn.on #js"data" + (λ (data) (#js.u.pass-message iw data)))) + (#js.peer.on #js"connection" handle-connection)) + + (#js.u.add-peer-init-task init-task) + + (void))] + [deregister (λ () ;; TODO: implement this + #:with-this this + (void))] + [invoke (λ (state evt) + #:with-this this + (define conn (iworld-conn #js.evt.iWorld)) + (#js.conn.on #js"open" + (λ (_) + (#js.u.change-state + (cb state #js.evt.iWorld)))) + #t)]))) + +(define (on-disconnect cb) + (λ (u) + (define on-disconnect-evt ($/obj [type #js"on-disconnect"])) + ($/obj + [name #js"on-disconnect"] + [register (λ () + #:with-this this + (#js.u.-disconnect-tasks.push + (λ (iworld) + (#js.u.queue-event ($/obj [type #js"on-disconnect"] + [iWorld iworld])))) + (void))] + [deregister (λ () ; TODO: implement this? maybe? + #:with-this this + (void))] + [invoke (λ (state evt) + #:with-this this + (#js.u.change-state (cb state #js.evt.iWorld)) + (void))]))) + +(define (server-id id) + (λ (u) + ($/obj + [name #js"server-id"] + [register (λ () + #:with-this this + (:= #js.u.-peer-id (js-string id)) + (void))] + [deregister (λ () + #:with-this this + (void))]))) + +(define (on-msg cb) + (λ (u) + (define on-msg-evt ($/obj [type #js"on-msg"])) + ($/obj + [name #js"on-msg"] + [register (λ () + #:with-this this + (define (handle-msg sender data) + (#js.u.queue-event ($/obj [type #js"on-msg"] + [iWorld sender] + [msg data]))) + (#js.u.-message-listeners.push handle-msg) + (void))] + [deregister (λ () ;; TODO: implement this + #:with-this this + (void))] + [invoke (λ (state evt) + (#js.u.change-state (cb state #js.evt.iWorld (decode-data #js.evt.msg))) + #t)]))) \ No newline at end of file diff --git a/racketscript-extras/racketscript/htdp/private/peer-universe/util.rkt b/racketscript-extras/racketscript/htdp/private/peer-universe/util.rkt new file mode 100644 index 00000000..0ef0859e --- /dev/null +++ b/racketscript-extras/racketscript/htdp/private/peer-universe/util.rkt @@ -0,0 +1,42 @@ +#lang racketscript/base + +(require (for-syntax racketscript/base + syntax/parse)) + +(provide format-js-str + (all-defined-out)) + +(define-syntax-rule (format-js-str fmt-str args ...) + (js-string (format fmt-str args ...))) + +(define (js-string? s) + (or ($/typeof s "string") ($/instanceof s #js*.String))) + +;; NOTE: because every racket datatype in +;; racketscript is stored as a js object, +;; ($/typeof obj ) +;; will always be true +(define (js-object? obj) + (and (not (string? obj) + (number? obj) + (boolean? obj) + (list? obj) + (symbol? obj)) + ($/typeof obj "object"))) + +(define (null? val) + ($/binop === val $/null)) + +(define (undefined? val) + ($/binop === val $/undefined)) + +(define (js-array? arr) + (#js*.Array.isArray arr)) + +(define (msg->string msg) + (cond [(undefined? msg) "undefined"] + [(js-string? msg) (js-string->string msg)] + [(or (js-object? msg) + (js-array? msg) + (null? msg)) (#js*.JSON.stringify msg)] + [else (format "~a" msg)])) \ No newline at end of file