Skip to content

Commit

Permalink
Merge pull request #4423 from gemini-hlsw/internalize-japgolly
Browse files Browse the repository at this point in the history
Internalize japgolly's IndexedDB library
  • Loading branch information
rpiaggio authored Dec 23, 2024
2 parents 5a6de77 + d9e6902 commit fe8fa27
Show file tree
Hide file tree
Showing 24 changed files with 2,776 additions and 9 deletions.
3 changes: 1 addition & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,7 @@ lazy val workers = project
.settings(
libraryDependencies ++= LucumaCatalog.value ++
Http4sDom.value ++
Log4Cats.value ++
ScalaWebAppUtil.value,
Log4Cats.value,
Test / scalaJSLinkerConfig ~= {
import org.scalajs.linker.interface.OutputPatterns
_.withOutputPatterns(OutputPatterns.fromJSFile("%s.mjs"))
Expand Down
6 changes: 0 additions & 6 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -335,10 +335,4 @@ object Dependencies {
)(scalaJsReact)
)

val ScalaWebAppUtil = Def.setting(
deps(
"com.github.japgolly.webapp-util" %%% "core",
"com.github.japgolly.webapp-util" %%% "core-boopickle"
)(webAppUtil)
)
}
1 change: 0 additions & 1 deletion project/Versions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,4 @@ object Versions {
val scalaCollectionContrib = "0.4.0"
val scalaJsDom = "2.8.0"
val scalaJsReact = "3.0.0-beta7"
val webAppUtil = "2.0.0-RC12"
}
244 changes: 244 additions & 0 deletions workers/src/main/scala/japgolly/webapputil/binary/BinaryData.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package japgolly.webapputil.binary

import cats.Eq
import japgolly.webapputil.general.ErrorMsg

import java.io.OutputStream
import java.lang.StringBuilder as JStringBuilder
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.util.Arrays
import java.util.Base64
import scala.collection.immutable.ArraySeq

object BinaryData extends BinaryData_PlatformSpecific_Object {

implicit def univEq: Eq[BinaryData] =
Eq.fromUniversalEquals

final val DefaultByteLimitInDesc = 50

def empty: BinaryData =
unsafeFromArray(new Array(0))

def byte(b: Byte): BinaryData = {
val a = new Array[Byte](1)
a(0) = b
unsafeFromArray(a)
}

def fromArray(a: Array[Byte]): BinaryData = {
val a2 = Arrays.copyOf(a, a.length)
unsafeFromArray(a2)
}

def fromArraySeq(a: ArraySeq[Byte]): BinaryData =
unsafeFromArray(a.unsafeArray.asInstanceOf[Array[Byte]])

def fromBase64(base64: String): Either[ErrorMsg, BinaryData] =
try
Right(fromBase64OrThrow(base64))
catch {
case e: IllegalArgumentException =>
Left(ErrorMsg("Invalid base64 data: " + e.getMessage))
}

def fromBase64OrThrow(base64: String): BinaryData =
unsafeFromArray(Base64.getDecoder.decode(base64))

def fromByteBuffer(bb: ByteBuffer): BinaryData =
if (bb.hasArray) {
val offset = bb.arrayOffset()
val a = Arrays.copyOfRange(bb.array(), offset, offset + bb.limit())
unsafeFromArray(a)
} else {
val a = new Array[Byte](bb.remaining)
bb.get(a)
unsafeFromArray(a)
}

def fromHex(hex: String): BinaryData = {
assert((hex.length & 1) == 0, "Hex strings must have an even length.")
var i = hex.length >> 1
val bytes = new Array[Byte](i)
while (i > 0) {
i -= 1
val si = i << 1
val byteStr = hex.substring(si, si + 2)
val byte = java.lang.Integer.parseUnsignedInt(byteStr, 16).byteValue()
bytes(i) = byte
}
unsafeFromArray(bytes)
}

/**
* unsafe because the array could be modified later and affect the underlying array we use here
*/
def unsafeFromArray(a: Array[Byte]): BinaryData =
new BinaryData(a, 0, a.length)

/**
* unsafe because the ByteBuffer could be modified later and affect the underlying array we use
* here
*/
def unsafeFromByteBuffer(bb: ByteBuffer): BinaryData =
if (bb.hasArray)
new BinaryData(bb.array(), bb.arrayOffset(), bb.limit())
else
fromByteBuffer(bb)

def fromStringAsUtf8(str: String): BinaryData =
unsafeFromArray(str.getBytes(StandardCharsets.UTF_8))
}

/** Immutable blob of binary data. */
final class BinaryData(
private[BinaryData] val bytes: Array[Byte],
private[BinaryData] val offset: Int,
val length: Int
) extends BinaryData_PlatformSpecific_Instance {

private val lastIndExcl = offset + length

// Note: It's acceptable to have excess bytes beyond the declared length
assert(lastIndExcl <= bytes.length,
s"offset($offset) + length ($length) exceeds number of bytes (${bytes.length})"
)

override def toString = s"BinaryData(${describe()})"

override def hashCode =
// Should use Arrays.hashCode() but have to copy to use provided length instead of array.length
offset * -947 + length

override def equals(o: Any): Boolean =
o match {
case b: BinaryData =>
@inline def sameRef = this eq b
@inline def sameLen = length == b.length
@inline def sameBin =
(0 until length).forall(i => bytes(offset + i) == b.bytes(b.offset + i))
sameRef || (sameLen && sameBin)
case _ =>
false
}

@inline def isEmpty: Boolean =
length == 0

@inline def nonEmpty: Boolean =
length != 0

def duplicate: BinaryData =
BinaryData.unsafeFromArray(toNewArray)

def describe(byteLimit: Int = BinaryData.DefaultByteLimitInDesc, sep: String = ",") = {
val byteDesc = describeBytes(byteLimit, sep)
val len = "%,d".format(length)
s"$len bytes: $byteDesc"
}

def describeBytes(limit: Int = BinaryData.DefaultByteLimitInDesc, sep: String = ",") = {
var i = bytes.iterator.drop(offset).map(b => "%02X".format(b & 0xff))
if (length > limit)
i = i.take(limit) ++ Iterator.single("")
else
i = i.take(length)
i.mkString(sep)
}

def writeTo(os: OutputStream): Unit =
os.write(bytes, offset, length)

// Note: the below must remain a `def` because ByteBuffers themselves have mutable state
/** unsafe in that the underlying bytes could be modified via access to unsafeArray */
def unsafeByteBuffer: ByteBuffer =
if (offset > 0)
ByteBuffer.wrap(bytes, 0, lastIndExcl).position(offset).slice()
else
ByteBuffer.wrap(bytes, 0, length)

def toNewByteBuffer: ByteBuffer =
ByteBuffer.wrap(toNewArray, 0, length)

def toNewArray: Array[Byte] =
Arrays.copyOfRange(bytes, offset, lastIndExcl)

/** unsafe in that you might get back the underlying array which is mutable */
lazy val unsafeArray: Array[Byte] =
if (offset == 0 && length == bytes.length)
bytes
else
toNewArray

def binaryLikeString: String = {
val chars = new Array[Char](length)
var j = length
while (j > 0) {
j -= 1
val b = bytes(offset + j)
val i = b.toInt & 0xff
chars.update(j, i.toChar)
}
String.valueOf(chars)
}

def hex: String =
bytes.iterator
.slice(offset, lastIndExcl)
.map(b => "%02X".format(b & 0xff))
.mkString

def ++(that: BinaryData): BinaryData =
if (this.isEmpty)
that
else if (that.isEmpty)
this
else {
val a = new Array[Byte](length + that.length)
Array.copy(this.bytes, this.offset, a, 0, this.length)
Array.copy(that.bytes, that.offset, a, this.length, that.length)
BinaryData.unsafeFromArray(a)
}

def drop(n: Int): BinaryData = {
val m = n.min(length)
new BinaryData(bytes, offset + m, length - m)
}

def take(n: Int): BinaryData = {
val m = n.min(length)
new BinaryData(bytes, offset, m)
}

def dropRight(n: Int): BinaryData = {
val m = n.min(length)
take(length - m)
}

def takeRight(n: Int): BinaryData = {
val m = n.min(length)
drop(length - m)
}

def toBase64: String =
Base64.getEncoder.encodeToString(unsafeArray)

def appendBase64(sb: JStringBuilder): Unit = {
val b64 = Base64.getEncoder.encode(unsafeArray)
var i = 0
while (i < b64.length) {
sb.append(b64(i).toChar)
i += 1
}
}

@inline def appendBase64(sb: StringBuilder): Unit =
appendBase64(sb.underlying)

def toStringAsUtf8: String =
new String(unsafeArray, StandardCharsets.UTF_8)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package japgolly.webapputil.binary

// **********
// * *
// * JS *
// * *
// **********

import org.scalajs.dom.Blob

import scala.scalajs.js
import scala.scalajs.js.JSConverters.*
import scala.scalajs.js.typedarray.ArrayBuffer
import scala.scalajs.js.typedarray.Uint8Array

trait BinaryData_PlatformSpecific_Object { self: BinaryData.type =>

def fromArrayBuffer(ab: ArrayBuffer): BinaryData =
BinaryData.fromByteBuffer(BinaryJs.arrayBufferToByteBuffer(ab))

def fromUint8Array(a: Uint8Array): BinaryData =
fromArrayBuffer(BinaryJs.uint8ArrayToArrayBuffer(a))

def unsafeFromArrayBuffer(ab: ArrayBuffer): BinaryData =
BinaryData.unsafeFromByteBuffer(BinaryJs.arrayBufferToByteBuffer(ab))

def unsafeFromUint8Array(a: Uint8Array): BinaryData =
unsafeFromArrayBuffer(BinaryJs.uint8ArrayToArrayBuffer(a))
}

trait BinaryData_PlatformSpecific_Instance { self: BinaryData =>

def toArrayBuffer: ArrayBuffer =
BinaryJs.byteBufferToArrayBuffer(self.unsafeByteBuffer)

def toUint8Array: Uint8Array =
new Uint8Array(toArrayBuffer)

def toBlob: Blob =
BinaryJs.byteBufferToBlob(self.unsafeByteBuffer)

def toNewJsArray: js.Array[Byte] =
self.toNewArray.toJSArray

def unsafeArrayBuffer: js.typedarray.ArrayBufferView =
BinaryJs.byteBufferToInt8Array(self.unsafeByteBuffer)

def unsafeUint8Array: Uint8Array =
new Uint8Array(toArrayBuffer)

def unsafeBlob: Blob =
BinaryJs.byteBufferToBlob(self.unsafeByteBuffer)

def unsafeJsArray: js.Array[Byte] =
self.unsafeArray.toJSArray
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package japgolly.webapputil.binary

import japgolly.scalajs.react.AsyncCallback

/** A means of converting instances of type `A` to a binary format and back. */
final class BinaryFormat[A](
val encode: A => AsyncCallback[BinaryData],
val decode: BinaryData => AsyncCallback[A]
) {

def xmap[B](onDecode: A => B)(onEncode: B => A): BinaryFormat[B] =
// Delegating because decoding can fail and must be wrapped to be pure
xmapAsync(a => AsyncCallback.delay(onDecode(a)))(b => AsyncCallback.delay(onEncode(b)))

def xmapAsync[B](onDecode: A => AsyncCallback[B])(
onEncode: B => AsyncCallback[A]
): BinaryFormat[B] =
BinaryFormat.async(decode(_).flatMap(onDecode))(onEncode(_).flatMap(encode))

type ThisIsBinary = BinaryFormat[A] =:= BinaryFormat[BinaryData]

// def encrypt(e: Encryption)(implicit ev: ThisIsBinary): BinaryFormat[BinaryData] =
// ev(this).xmapAsync(e.decrypt)(e.encrypt)

// def compress(c: Compression)(implicit ev: ThisIsBinary): BinaryFormat[BinaryData] =
// ev(this).xmap(c.decompressOrThrow)(c.compress)
}

object BinaryFormat {

val id: BinaryFormat[BinaryData] = {
val f: BinaryData => AsyncCallback[BinaryData] = AsyncCallback.pure
async(f)(f)
}

def apply[A](decode: BinaryData => A)(encode: A => BinaryData): BinaryFormat[A] =
async(b => AsyncCallback.delay(decode(b)))(a => AsyncCallback.delay(encode(a)))

def async[A](decode: BinaryData => AsyncCallback[A])(
encode: A => AsyncCallback[BinaryData]
): BinaryFormat[A] =
new BinaryFormat(encode, decode)
}
Loading

0 comments on commit fe8fa27

Please sign in to comment.