Skip to content

Commit

Permalink
feat: i am frontend, i write javascript
Browse files Browse the repository at this point in the history
  • Loading branch information
theprimeagen committed May 21, 2024
1 parent 7280907 commit b096502
Show file tree
Hide file tree
Showing 16 changed files with 551 additions and 14 deletions.
4 changes: 3 additions & 1 deletion examples/v2/doom/run/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/theprimeagen/vim-with-me/pkg/v2/ascii_buffer"
"github.com/theprimeagen/vim-with-me/pkg/v2/assert"
"github.com/theprimeagen/vim-with-me/pkg/v2/encoder"
"github.com/theprimeagen/vim-with-me/pkg/v2/net"
"github.com/theprimeagen/vim-with-me/pkg/v2/relay"

//"github.com/theprimeagen/vim-with-me/pkg/v2/encoding"
Expand Down Expand Up @@ -44,7 +45,8 @@ func (r *RelayClient) send(frame *encoder.EncodingFrame) {
}

fmt.Printf("sending frame into relay(%d): %d\n", len(r.cache), frame.Len)
n, err := frame.Into(r.cache, 0)
frameable := net.Frameable{Item: frame}
n, err := frameable.Into(r.cache, 0)
assert.NoError(err, "relay server could not call frame#into")

err = r.client.Relay(r.cache[:n])
Expand Down
14 changes: 11 additions & 3 deletions js/app.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import decode from "./decode/index.js"
import { parseFrame } from "./net/frame.js"
import { WS } from "./ws/index.js"

// TODO: provide a url?

/**
* @param {HTMLElement} el
*/
function run(el) {
console.log("here i am")
decode.test()
const ws = new WS("ws://localhost:8080/ws")

ws.onMessage(async function(blob) {
const buf = new Uint8Array(await blob.arrayBuffer())
const frame = parseFrame(buf)
console.log(frame.cmd, frame.data.byteLength)
})
}

run(document.body)
23 changes: 23 additions & 0 deletions js/assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @param {any} truthy
* @param {string} msg
* */
export function debugAssert(truthy, msg) {
if (!truthy) {
debugger
console.error(msg)
}
}

/**
* no idea how to make that work...
* @param {any} truthy
* @param {string} msg
* @returns {asserts truthy is true}
* */
export function assert(truthy, msg) {
if (!truthy) {
throw new Error(msg)
}
}

9 changes: 9 additions & 0 deletions js/bytes/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

/**
* @param {Uint8Array} buf
* @param {number} offset
*/
export function read16(buf, offset) {
return (buf[offset] << 8) + buf[offset + 1]
}

48 changes: 48 additions & 0 deletions js/bytes/writer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { assert, debugAssert } from "../assert"

export class EightBitWriter {
/** @type {Uint8Array | null} */
#buffer = null

/** @type {number} */
#idx

constructor() {
this.#idx = 0
}

data() {
return this.#buffer?.slice(0, this.#idx) ?? new Uint8Array(0)
}

len() {
return this.#idx
}

/** @param {Uint8Array} buf */
reset(buf) {
this.#idx = 0
this.#buffer = buf
}

/**
* @param {number} num
* @returns {boolean}
**/
write(num) {
assert(this.#buffer !== null, "expected buffer to not equal null")

// TODO: figure out how to make assert "type safe"
if (this.#buffer === null) {
return false
}

if (this.#buffer.byteLength === this.#idx) {
return false
}

this.#buffer[this.#idx++] = num
return true
}
}

15 changes: 15 additions & 0 deletions js/cmds.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const types = {
open: 0,
brightnessToAscii: 1,
frame: 2,
}

export const encodings = {
NONE: 1,
XOR_RLE: 2,
HUFFMAN: 3,
XOR_BIT_DIFF: 4, // Not implement, but i am horned up for it
XOR_HUFFMAN: 5,
HUFFMAN_QUADTREE: 6, // Maybe implement?
XOR_RLE_QUADTREE: 7, // Maybe implement?
}
194 changes: 194 additions & 0 deletions js/decode/frame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/** @typedef {import("./types.ts").DecodeFrame} DecodeFrame */

import { assert } from "../assert.js";
import { read16 } from "../bytes/utils.js";
import { types, encodings } from "../cmds.js";
import { scratchArr, scratchWriter8Bit } from "../scratch.js";

/**
* @param {import("../types.ts").Frame} frame
* @return DecodeFrame | null
*/
export function createFrame(frame) {
if (frame.cmd !== types.frame) {
return null
}

return { frame }
}

/**
* @param {DecodeFrame} decode
* @return {boolean}
*/
function isHuffmanEncoded(decode) {
return decode.frame.data[0] === encodings.HUFFMAN
}

/**
* @param {DecodeFrame} decode
* @return {boolean}
*/
function isXOR_RLE(decode) {
return decode.frame.data[0] === encodings.XOR_RLE
}

/**
* @param {Uint8Array} decoder
* @param {number} idx
* @returns {number}
**/
function left(decoder, idx) {
assert(decoder.byteLength > idx + 5, "decoder length + idx is shorter than huffmanNode decode length")
return read16(decoder, idx + 2)
}

/**
* @param {Uint8Array} decoder
* @param {number} idx
* @returns {number}
**/
function right(decoder, idx) {
assert(decoder.byteLength > idx + 5, "decoder length + idx is shorter than huffmanNode decode length")
return read16(decoder, idx + 4)
}

/**
* @param {Uint8Array} decoder
* @param {number} idx
* @param {number} bit
* @returns {number}
**/
function jump(decoder, idx, bit) {
if (bit === 1) {
return right(decoder, idx)
}
return left(decoder, idx)
}

/**
* @param {Uint8Array} decoder
* @param {number} idx
* @returns {number}
**/
function value(decoder, idx) {
return read16(decoder, idx)
}

/**
* @param {Uint8Array} decoder
* @param {number} idx
* @returns {boolean}
**/
function isLeaf(decoder, idx) {
assert(decoder.byteLength > idx + 5, "decoder length + idx is shorter than huffmanNode decode length")
return read16(decoder, idx + 2) == 0 &&
read16(decoder, idx + 4) == 0
}

/**
* @param {Uint8Array} decodingTree
* @param {Uint8Array} data
* @param {number} bitLength
* @param {import("../types.ts").ByteWriter} writer
*/
function decodeHuffman(decodingTree, data, bitLength, writer) {
assert(data.byteLength >= bitLength/8 + 1, "you did not provide enough data")

let idx = 0
let decodeIdx = 0

outer:
while (true) {
for (let bitIdx = 7; bitIdx >= 0; bitIdx--) {
const bit = (data[idx] >> bitIdx) & 0x1
bitLength--

decodeIdx = jump(decodingTree, decodeIdx, bit)

if (isLeaf(decodingTree, decodeIdx)) {

if (!writer.write(value(decodingTree, decodeIdx))) {
throw new Error("unable to write value into buffer")
}

decodeIdx = 0
}

if (bitLength === 0) {
break outer
}
}

idx++
}
}

/**
* @param {DecodeFrame} decode
*/
function expandHuffman(decode) {
// 1 byte to encoding type (huffman)
// 1 + 2 bytes bitLen
// 3 + 2 bytes decodingTreeLength

const bitLen = read16(decode.decodeFrame, 1)
const decodingTreeLength = read16(decode.decodeFrame, 3)
const decodingTree = decode.decodeFrame.subarray(5, 5 + decodingTreeLength)
const writer = scratchWriter8Bit()

decodeHuffman(decodingTree, decode.decodeFrame, bitLen, writer)
decode.decodeFrame = writer.data()

assert(decode.decodeFrame.byteLength > 0, "decoding failed")
}


/**
* @param {DecodeFrame} decode
*/
function expandXOR_RLE(decode) {
if (decode.prevDecodeFrame === null) {
return
}

let idx = 0
const data = decode.frame.data
for (let i = 1; i < data.length; i += 2) {
const repeat = data[i]
const char = data[i + 1]
for (let count = 0; count < repeat; count++, idx++) {
scratchArr[idx] = char ^ decode.prevDecodeFrame[idx]
}
}

// TODO: Copy within?
decode.decodeFrame = scratchArr.slice(0, idx)
}

/**
* @param {DecodeFrame} decode
*/
export function expand(decode) {
if (isXOR_RLE(decode)) {
expandXOR_RLE(decode)
} else {
expandHuffman(decode)
}
}

/**
* @param {DecodeFrame} decode
* @return {Uint8Array}
*/
export function asciiPixel(decode) {
const frame = decode.decodeFrame
const out = new Uint8Array(frame.byteLength)
for (let j = 0, i = 0; i < frame.byteLength; ++i, j += 2) {
out[j] = frame[i]
out[j + 1] = frame[i]
}
return out
}


8 changes: 0 additions & 8 deletions js/decode/index.js

This file was deleted.

8 changes: 8 additions & 0 deletions js/decode/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Frame } from "../types"

export type DecodeFrame = {
frame: Frame,
decodeFrame: Uint8Array,
prevDecodeFrame: Uint8Array | null,
length: number,
}
25 changes: 25 additions & 0 deletions js/net/frame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { debugAssert } from "../assert.js"
import { read16 } from "../bytes/utils.js"

const VERSION = 1
const HEADER_SIZE = 4

/**
* @param {Uint8Array} buf
* @return {import("../types.ts").Frame}
* */
export function parseFrame(buf) {
const v = VERSION + 8
const D = buf[0]
debugAssert(v - 8===D, "the frame received doesn't have version alignment")
const cmd = buf[1]

const len = read16(buf, 2)

debugAssert(buf.byteLength - HEADER_SIZE === len, "the frame received doesn't have version alignment")

return {
cmd,
data: buf.subarray(HEADER_SIZE),
}
}
11 changes: 11 additions & 0 deletions js/scratch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { EightBitWriter } from "./bytes/writer"
export const scratchBuff = new ArrayBuffer(1024 * 1024)
export const scratchArr = new Uint8Array(scratchBuff)

const writer = new EightBitWriter()
/** @returns {import("./types").ByteWriter} */
export function scratchWriter8Bit() {
writer.reset(scratchArr)
return writer
}

Loading

0 comments on commit b096502

Please sign in to comment.