class Msg constructor: () -> @header = { sid: 0, cid: 0, type: 0, size: 0 } @data = undefined as_raw:() -> length = 13 + @header.size arr = new Uint8Array(length) arr.set(Msg.MAGIC_START, 0) arr[2] = @header.type bytes = Msg.bytes_of @header.cid arr.set(bytes,3) bytes = Msg.bytes_of @header.sid arr.set(bytes,5) bytes = Msg.bytes_of @header.size, 4 arr.set(bytes,7) if @data arr.set(@data, 11) arr.set(Msg.MAGIC_END, @header.size + 11) arr.buffer Msg.decode = (raw) -> new Promise (resolve, reject) -> msg = new Msg() if(Msg.int_from(Msg.MAGIC_START, 0) != Msg.int_from(raw, 0)) return reject("Unmatch message begin magic number") msg.header.type = raw[2] msg.header.cid = Msg.int_from(raw, 3) msg.header.sid = Msg.int_from(raw,5) msg.header.size = Msg.int_from(raw, 7,4) msg.data = raw.slice(11, 11+msg.header.size) if(Msg.int_from(Msg.MAGIC_END, 0) != Msg.int_from(raw, 11+msg.header.size)) return reject("Unmatch message end magic number") resolve msg Msg.bytes_of = (x,s) -> s = 2 unless s is 4 bytes=new Uint8Array(s) bytes[0]=x & (255) x=x>>8 bytes[1]=x & (255) return bytes unless s is 4 x=x>>8 bytes[2]=x & (255) x=x>>8 bytes[3]=x & (255) return bytes Msg.int_from = (bytes, offset, size) -> return (bytes[offset] | (bytes[offset+1]<<8)) unless size is 4 return (bytes[offset] | (bytes[offset+1]<<8) | (bytes[offset+2]<<16) | (bytes[offset+3] << 24)) Msg.OK = 0 Msg.ERROR = 1 Msg.DATA = 6 Msg.CLOSE = 5 Msg.SUBSCRIBE = 2 Msg.UNSUBSCRIBE = 3 Msg.CTRL = 7 Msg.PING = 8 Msg.MAGIC_END = [0x44, 0x54] Msg.MAGIC_START = [0x4e, 0x41 ] class Subscriber constructor: (@channel) -> @id = undefined @channel_id = undefined @onmessage = undefined @onerror = undefined @onopen = undefined @onclose = undefined @tunnel = undefined @is_opened = false send: (type, arr) -> if not @tunnel @onerror "Tunnel is not opened" if @onerror return if not @is_opened @onerror "Channel is not opened yet" if @onerror return @tunnel.send @genmsg type, arr genmsg: (type, data) -> msg = new Msg() msg.header.sid = @id msg.header.cid = @channel_id msg.header.type = type msg.header.size = if data then data.length else 0 msg.data = data msg close: (b) -> @is_opened = false return unless @tunnel @tunnel.unsubscribe @, b class AntunnelApi constructor: (@uri) -> @socket = undefined @pending = {} @subscribers = {} @onclose = undefined ready: () -> return new Promise (resolve, reject) => return reject() if not @uri return resolve() if @socket isnt undefined # connect to the socket console.log "Connect to #{@uri}" @socket = new WebSocket(@uri) @socket.binaryType = 'arraybuffer' @socket.onmessage = (evt) => @process evt @socket.onclose = (evt) => @socket = undefined for k,v of @pending v.tunnel = undefined v.onclose() if v.onclose for k,v of @subscribers v.tunnel = undefined v.is_opened = false v.onclose() if v.onclose @pending = {} @subscribe = {} @onclose() if @onclose() @socket.onerror = (evt) => v.onerror(evt.toString()) for k,v of @pending when v.onerror v.onerror(evt.toString()) for k,v of @subscribers when v.onerror @socket.onopen = (e) => resolve() process: (evt) -> Msg.decode(new Uint8Array(evt.data)).then (msg) => # find the correct subscriber of the data relay_msg = (m, a) => sub = @pending[m.header.sid] if sub sub[a] m if sub[a] return sub = @subscribers[m.header.sid] if sub sub[a] m if sub[a] switch msg.header.type when Msg.OK # first look for the pending sub = @pending[msg.header.sid] if sub delete @pending[msg.header.sid] sub.id = Msg.int_from(msg.data,0) sub.channel_id = msg.header.cid @subscribers[sub.id] = sub sub.is_opened = true sub.onopen() if sub.onopen else relay_msg msg, "onmessage" when Msg.DATA relay_msg msg, "onmessage" when Msg.CTRL relay_msg msg, "onctrl" when Msg.ERROR relay_msg msg, "onerror" when Msg.UNSUBSCRIBE sub = @subscribers[msg.header.sid] return unless sub sub.close(true) when Msg.PING # do nothing else console.error "Message of type #{msg.header.type} is unsupported", msg .catch (e) => v.onerror(e) for k,v of @pending when v.onerror v.onerror(e) for k,v of @subscribers when v.onerror console.log e subscribe: (sub) -> @ready().then ()=> # insert it to pending list sub.tunnel = @ sub.id = Math.floor(Math.random()*1000) + 1 while @subscribers[sub.id] or @pending[sub.id] sub.id = Math.floor(Math.random()*1000) + 1 @pending[sub.id] = sub # send request to connect to a channel @send sub.genmsg Msg.SUBSCRIBE, (new TextEncoder()).encode(sub.channel) .catch (e) -> sub.onerror e.toString() if sub.onerror unsubscribe: (sub, b) -> @ready().then ()=> return unless @subscribers[sub.id] # insert it to pending list # send request to connect to a channel @send sub.genmsg Msg.UNSUBSCRIBE, undefined if not b sub.onclose() if sub.onclose delete @subscribers[sub.id] sub.tunnel = undefined sub.is_opened = false .catch (e) -> sub.onerror e.toString() if sub.onerror send: (msg) -> # return unless @subscribers[msg.header.sid] @socket.send msg.as_raw() close: () -> console.log "Close connection to #{@uri}" @socket.close() if @socket @onclose() if @onclose() W = this if not W.Antunnel W.Antunnel = { tunnel: undefined init: ((url) -> return new Promise (resolve, reject) -> return resolve(W.Antunnel.tunnel) if W.Antunnel.tunnel W.Antunnel.tunnel = new AntunnelApi(url) W.Antunnel.tunnel.onclose = () -> W.Antunnel.tunnel = undefined W.Antunnel.tunnel.ready().then () -> resolve(W.Antunnel.tunnel) .catch (e) -> reject(e)), Subscriber: Subscriber, Msg: Msg }