All Downloads are FREE. Search and download functionalities are using the official Maven repository.

japgolly.webapputil.indexeddb.IndexedDb.scala Maven / Gradle / Ivy

There is a newer version: 2.0.0-RC12
Show newest version
package japgolly.webapputil.indexeddb

import cats.kernel.Eq
import japgolly.scalajs.react._
import japgolly.scalajs.react.util.Util.{identity => identityFn}
import japgolly.univeq.UnivEqCats._
import japgolly.webapputil.indexeddb.TxnMode._
import org.scalajs.dom._
import scala.annotation.elidable
import scala.scalajs.js
import scala.util.{Failure, Success, Try}

final class IndexedDb(raw: IDBFactory) {
  import IndexedDb._
  import IndexedDb.Internals._

  def open(name: DatabaseName): OpenResult =
    _open(raw.open(name.value))

  def open(name: DatabaseName, version: Int): OpenResult =
    _open(raw.open(name.value, version))

  private def _open(rawOpen: => IDBOpenDBRequest[IDBDatabase]): OpenResult =
    callbacks => {

      def create(): IDBOpenDBRequest[IDBDatabase] = {
        val r = rawOpen

        // r.onblocked = callbacks.blocked.toJsFn1

        r.onupgradeneeded = e => {
          val db = new DatabaseInVersionChange(r.result)
          val args = versionChange(db, e)
          callbacks.upgradeNeeded(args).runNow()
        }

        r
      }

      asyncRequest(create()) { r =>
        val rawDb = r.result

        rawDb.onversionchange = e => {
          try {
            val args = versionChange(new DatabaseInVersionChange(rawDb), e)
            callbacks.versionChange(args).runNow()
          } finally {
            // We close the DB at the end of this event on matter what so that other connections don't block and we
            // don't have to handle onblocked events.
            rawDb.close()

            // The onclose event handler is only fired "when the database is unexpectedly closed".
            // Therefore we call it here explicitly.
            // https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/onclose
            callbacks.closed.runNow()
          }
        }

        rawDb.onclose = _ => {
          callbacks.closed.runNow()
        }

        new Database(rawDb, onClose = callbacks.closed)
      }
    }

  def deleteDatabase(name: DatabaseName): AsyncCallback[Unit] =
    asyncRequest_(raw.deleteDatabase(name.value))
}

// =====================================================================================================================

object IndexedDb {

  def apply(raw: IDBFactory): IndexedDb =
    new IndexedDb(raw)

  def global(): Option[IndexedDb] =
    try {
      window.indexedDB.toOption.map(apply)
    } catch {
      case _: Throwable => None
    }

  // ===================================================================================================================
  // Main types and classes

  import Internals._

  final case class DatabaseName(value: String)

  type OpenResult = OpenCallbacks => AsyncCallback[Database]

  /** Callbacks to install when opening a DB.
   *
   * Note 1: On `versionChange`, the DB connection will be closed automatically.
   *
   * Note 2: There's no `blocked` handler because we currently don't allow blocking. To quote the idb spec:
   *         if "there are open connections that don’t close in response to a versionchange event, the request will be
   *         blocked until all they close".
   */
  final case class OpenCallbacks(upgradeNeeded: VersionChange => Callback,
                                 versionChange: VersionChange => Callback = _ => Callback.empty,
                                 closed       : Callback                  = Callback.empty)

  final case class Error(event: ErrorEvent) extends RuntimeException(
    event.asInstanceOf[js.Dynamic].message.asInstanceOf[js.UndefOr[String]].getOrElse(null)
  ) {

    // Note: allowing .message to be undefined is presumably only required due to use of fake-indexeddb in tests
    val msg: String =
      event.asInstanceOf[js.Dynamic].message.asInstanceOf[js.UndefOr[String]].getOrElse("")

    @elidable(elidable.FINEST)
    override def toString =
      s"IndexedDb.Error($msg)"

    def isStoredDatabaseHigherThanRequested: Boolean = {
      // Chrome: The requested version (1) is less than the existing version (2).
      // Firefox: The operation failed because the stored database is a higher version than the version requested.
      msg.contains("version") && (msg.contains("higher") || msg.contains("less than"))
    }
  }

  final case class VersionChange(db: DatabaseInVersionChange, oldVersion: Int, newVersion: Option[Int]) {
    def createObjectStore[K, V](defn: ObjectStoreDef[K, V], createdInDbVer: Int): Callback =
      Callback.when(oldVersion < createdInDbVer && newVersion.exists(_ >= createdInDbVer))(
        db.createObjectStore(defn))
  }

  final class DatabaseInVersionChange(raw: IDBDatabase) {
    def createObjectStore[K, V](defn: ObjectStoreDef[K, V]): Callback =
      Callback {
        raw.createObjectStore(defn.name)
      }
  }

  final class Database(raw: IDBDatabase, onClose: Callback) {

    def atomic[K, V](store: ObjectStoreDef.Async[K, V]): AtomicAsyncDsl[K, V] =
      new AtomicAsyncDsl(this, store)

    def close: Callback = {
      val actuallyClose =
        Callback(raw.close()).attempt

      // The onclose event handler is only fired "when the database is unexpectedly closed".
      // Therefore we call it here explicitly.
      // https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/onclose
      actuallyClose >> onClose
    }

    def compareAndSet(stores: ObjectStoreDef[_, _]*): CasDsl1 =
      new CasDsl1(this, stores)

    def transactionRO: RunTxnDsl1[RO] =
      new RunTxnDsl1(raw, TxnDslRO, IDBTransactionMode.readonly)

    def transactionRW: RunTxnDsl1[RW] =
      new RunTxnDsl1(raw, TxnDslRW, IDBTransactionMode.readwrite)

    // Convenience methods

    /** Note: insert only */
    def add[K, V](store: ObjectStoreDef.Async[K, V])(key: K, value: V): AsyncCallback[Unit] =
      store.encode(value).flatMap(add(store.sync)(key, _))

    /** Note: insert only */
    def add[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, value: V): AsyncCallback[Unit] =
      transactionRW(store)(_.objectStore(store).flatMap(_.add(key, value)))

    /** Note: insert only */
    def addWhenDefined[K, V](store: ObjectStoreDef.Async[K, V])(key: K, value: Option[V]): AsyncCallback[Unit] =
      AsyncCallback.traverseOption_(value)(add(store)(key, _))

    /** Note: insert only */
    def addWhenDefined[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, value: Option[V]): AsyncCallback[Unit] =
      AsyncCallback.traverseOption_(value)(add(store)(key, _))

    def clear[K, V](store: ObjectStoreDef[K, V]): AsyncCallback[Unit] =
      transactionRW(store)(_.objectStore(store.sync).flatMap(_.clear))

    def delete[K, V](store: ObjectStoreDef[K, V])(key: K): AsyncCallback[Unit] =
      transactionRW(store)(_.objectStore(store.sync).flatMap(_.delete(key)))

    def get[K, V](store: ObjectStoreDef.Async[K, V])(key: K): AsyncCallback[Option[V]] =
      get(store.sync)(key).flatMap(AsyncCallback.traverseOption(_)(_.decode))

    def get[K, V](store: ObjectStoreDef.Sync[K, V])(key: K): AsyncCallback[Option[V]] =
      transactionRO(store)(_.objectStore(store).flatMap(_.get(key)))

    def getAllKeys[K, V](store: ObjectStoreDef[K, V]): AsyncCallback[Vector[K]] =
      transactionRO(store)(_.objectStore(store.sync).flatMap(_.getAllKeys))

    def getAllValues[K, V](store: ObjectStoreDef.Async[K, V]): AsyncCallback[Vector[V]] =
      getAllValues(store.sync).flatMap(AsyncCallback.traverse(_)(_.decode))

    def getAllValues[K, V](store: ObjectStoreDef.Sync[K, V]): AsyncCallback[Vector[V]] =
      transactionRO(store)(_.objectStore(store).flatMap(_.getAllValues))

    /** aka upsert */
    def put[K, V](store: ObjectStoreDef.Async[K, V])(key: K, value: V): AsyncCallback[Unit] =
      store.encode(value).flatMap(put(store.sync)(key, _))

    /** aka upsert */
    def put[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, value: V): AsyncCallback[Unit] =
      transactionRW(store)(_.objectStore(store).flatMap(_.put(key, value)))

    /** aka upsert or delete */
    def putOrDelete[K, V](store: ObjectStoreDef.Async[K, V])(key: K, value: Option[V]): AsyncCallback[Unit] =
      value match {
        case Some(v) => put(store)(key, v)
        case None    => delete(store)(key)
      }

    /** aka upsert or delete */
    def putOrDelete[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, value: Option[V]): AsyncCallback[Unit] =
      value match {
        case Some(v) => put(store)(key, v)
        case None    => delete(store)(key)
      }

    /** aka upsert */
    def putWhenDefined[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, value: Option[V]): AsyncCallback[Unit] =
      AsyncCallback.traverseOption_(value)(put(store)(key, _))

    /** aka upsert */
    def putWhenDefined[K, V](store: ObjectStoreDef.Async[K, V])(key: K, value: Option[V]): AsyncCallback[Unit] =
      AsyncCallback.traverseOption_(value)(put(store)(key, _))
  } // class Database

  final class ObjectStore[K, V](val defn: ObjectStoreDef.Sync[K, V]) {
    import defn.{keyCodec, valueCodec}

    private implicit def autoWrap[M <: TxnMode, A](s: TxnStep[M, A]): Txn[M, A] =
      Txn(s)

    /** Note: insert only */
    def add(key: K, value: V): Txn[RW, Unit] = {
      val k = keyCodec.encode(key)
      TxnDslRW.eval(valueCodec.encode(value)).flatMap(TxnStep.StoreAdd(this, k, _))
    }

    /** Note: insert only */
    def addWhenDefined(key: K, value: Option[V]): Txn[RW, Unit] =
      value.fold[Txn[RW, Unit]](TxnStep.unit)(add(key, _))

    def clear: Txn[RW, Unit] =
      TxnStep.StoreClear(this)

    def delete(key: K): Txn[RW, Unit] =
      TxnStep.StoreDelete(this, keyCodec.encode(key))

    def get(key: K): Txn[RO, Option[V]] =
      TxnStep.StoreGet(this, keyCodec.encode(key))

    def getAllKeys: Txn[RO, Vector[K]] =
      TxnStep.StoreGetAllKeys(this)

    def getAllValues: Txn[RO, Vector[V]] =
      TxnStep.StoreGetAllVals(this)

    def modify(key: K)(f: V => V): Txn[RW, Option[V]] =
      get(key).flatMap {
        case Some(v1) =>
          val v2 = f(v1)
          put(key, v2).map(_ => Some(v2))
        case None =>
          TxnDslRW.none
      }

    def modifyOption(key: K)(f: Option[V] => Option[V]): Txn[RW, Option[V]] =
      for {
        o1 <- get(key)
        o2  = f(o1)
        _  <- putOrDelete(key, o2)
      } yield o2

    /** aka upsert */
    def put(key: K, value: V): Txn[RW, Unit] = {
      val k = keyCodec.encode(key)
      TxnDslRW.eval(valueCodec.encode(value)).flatMap(TxnStep.StorePut(this, k, _))
    }

    /** aka upsert or delete */
    def putOrDelete(key: K, value: Option[V]): Txn[RW, Unit] =
      value match {
        case Some(v) => put(key, v)
        case None    => delete(key)
      }

    /** aka upsert */
    def putWhenDefined(key: K, value: Option[V]): Txn[RW, Unit] =
      value.fold[Txn[RW, Unit]](TxnStep.unit)(put(key, _))
  }

  // ===================================================================================================================
  // DSLs

  final class AtomicAsyncDsl[K, V](db: Database, store: ObjectStoreDef.Async[K, V]) {

    /** Performs an async modification on a store value.
      *
      * This only modifies an existing value. Use [[modifyAsyncOption()]] to upsert and/or delete values.
      *
      * This uses [[compareAndSet()]] for atomicity and thread-safety.
      *
      * @return If the value exists, this returns the previous and updated values
      */
    def modify(key: K)(f: V => V): AsyncCallback[Option[(V, V)]] =
      modifyAsync(key)(v => AsyncCallback.pure(f(v)))

    /** Performs an async modification on a store value.
      *
      * This only modifies an existing value. Use [[modifyAsyncOption()]] to upsert and/or delete values.
      *
      * This uses [[compareAndSet()]] for atomicity and thread-safety.
      *
      * @return If the value exists, this returns the previous and updated values
      */
    def modifyAsync(key: K)(f: V => AsyncCallback[V]): AsyncCallback[Option[(V, V)]] =
      db
        .compareAndSet(store)
        .getValueAsync(store)(key)
        .mapAsync(AsyncCallback.traverseOption(_)(v1 => f(v1).map((v1, _))))
        .putResultWhenDefinedBy(store)(key, _.map(_._2))

    /** Performs an async modification on an optional store value.
      *
      * This uses [[compareAndSet()]] for atomicity and thread-safety.
      *
      * @return The previous and updated values
      */
    def modifyOption(key: K)(f: Option[V] => Option[V]): AsyncCallback[(Option[V], Option[V])] =
      modifyOptionAsync(key)(v => AsyncCallback.pure(f(v)))

    /** Performs an async modification on an optional store value.
      *
      * This uses [[compareAndSet()]] for atomicity and thread-safety.
      *
      * @return The previous and updated values
      */
    def modifyOptionAsync(key: K)(f: Option[V] => AsyncCallback[Option[V]]): AsyncCallback[(Option[V], Option[V])] =
      db
        .compareAndSet(store)
        .getValueAsync(store)(key)
        .mapAsync { o1 => f(o1).map((o1, _)) }
        .putOrDeleteResultBy(store)(key, _._2)
  }

  final class RunTxnDsl1[M <: TxnMode] private[IndexedDb](raw: IDBDatabase, txnDsl: TxnDsl[M], mode: IDBTransactionMode) {
    def apply(stores: ObjectStoreDef[_, _]*): RunTxnDsl2[M] =
      new RunTxnDsl2(raw, txnDsl, mode, mkStoreArray(stores))
  }

  final class RunTxnDsl2[M <: TxnMode] private[IndexedDb](raw: IDBDatabase, txnDsl: TxnDsl[M], mode: IDBTransactionMode, stores: js.Array[String]) {

    def apply[A](f: TxnDsl[M] => Txn[M, A]): AsyncCallback[A] = {
      val x = CallbackTo.pure(f(txnDsl))
      sync(_ => x)
    }

    def sync[A](dslCB: TxnDsl[M] => CallbackTo[Txn[M, A]]): AsyncCallback[A] = {

      @inline def startRawTxn(complete: Try[Unit] => Callback) = {
        val txn = raw.transaction(stores, mode)

        txn.onerror = event => {
          complete(Failure(Error(event))).runNow()
        }

        txn.oncomplete = complete(success_).toJsFn1

        txn
      }

      for {
        dsl <- dslCB(txnDsl).asAsyncCallback

        (awaitTxnCompletion, complete) <- AsyncCallback.promise[Unit].asAsyncCallback

        result <- AsyncCallback.suspend {
          val txn = startRawTxn(complete)
          interpretTxn(txn, dsl)
        }

        _ <- awaitTxnCompletion

      } yield result
    }

    def async[A](dsl: TxnDsl[M] => AsyncCallback[Txn[M, A]]): AsyncCallback[A] =
      // Note: This is safer than it looks.
      //       1) This is `Dsl => AsyncCallback[Txn[A]]`
      //          and not `Dsl => Txn[AsyncCallback[A]]`
      //       2) Everything within Txn is still synchronous and lawful
      //       3) Only one transaction is created (i.e. only one call to `apply`)
      dsl(txnDsl).flatMap(txnA => apply(_ => txnA))
  }

  final class CasDsl1(db: Database, stores: Seq[ObjectStoreDef[_, _]]) {

    def get[A](f: TxnDsl[RO] => Txn[RO, A])(implicit e: Eq[A]) =
      getAndCompareBy(f)(e.eqv)

    def getAndCompareBy_==[A](f: TxnDsl[RO] => Txn[RO, A]) =
      getAndCompareBy(f)(_ == _)

    def getAndCompareBy[A](f: TxnDsl[RO] => Txn[RO, A])(eql: (A, A) => Boolean) =
      new CasDsl2[A](db, stores, f(TxnDslRO), eql)

    /** Note: CAS comparison is on the raw IDB value, i.e. the result prior to async decoding */
    def getValueAsync[K, V](store: ObjectStoreDef.Async[K, V])(key: K): CasDsl3[Option[store.Value], Option[V]] =
      get(_.objectStore(store).flatMap(_.get(key)))
        .mapAsync(AsyncCallback.traverseOption(_)(_.decode))

    /** Note: CAS comparison is on `Option[V]`, i.e. the decoded result */
    def getValueSync[K, V](store: ObjectStoreDef.Sync[K, V])(key: K)(implicit e: Eq[Option[V]]): CasDsl2[Option[V]] =
      get(_.objectStore(store).flatMap(_.get(key)))

    def getAllKeys[K, V](store: ObjectStoreDef[K, V])(implicit e: Eq[Vector[K]]): CasDsl2[Vector[K]] =
      get(_.objectStore(store.sync).flatMap(_.getAllKeys))

    def getAllValuesAsync[K, V](store: ObjectStoreDef.Async[K, V]): CasDsl3[Vector[store.Value], Vector[V]] =
      get(_.objectStore(store).flatMap(_.getAllValues))
        .mapAsync(AsyncCallback.traverse(_)(_.decode))

    def getAllValuesSync[K, V](store: ObjectStoreDef.Sync[K, V])(implicit e: Eq[Vector[V]]): CasDsl2[Vector[V]] =
      get(_.objectStore(store.sync).flatMap(_.getAllValues))
  }

  sealed trait CasDsl23[A, B] {
    def mapAsync[C](f: B => AsyncCallback[C]): CasDsl3[A, C]

    final def map[C](f: B => C) =
      mapAsync(b => AsyncCallback.pure(f(b)))

    final def mapSync[C](f: B => CallbackTo[C]) =
      mapAsync(f(_).asAsyncCallback)
  }

  final class CasDsl2[A](db: Database, stores: Seq[ObjectStoreDef[_, _]], get: Txn[RO, A], eql: (A, A) => Boolean) extends CasDsl23[A, A] {

    private def next = mapAsync(AsyncCallback.pure)

    override def mapAsync[C](f: A => AsyncCallback[C]) =
      new CasDsl3[A, C](db, stores, get, eql, f)

    def set[C](set: TxnDsl[RW] => A => Txn[RW, C]): AsyncCallback[C] =
      next.set { dsl => (a, _) => set(dsl)(a) }
  }

  final class CasDsl3[A, B](db: Database, stores: Seq[ObjectStoreDef[_, _]], get: Txn[RO, A], eql: (A, A) => Boolean, prep: A => AsyncCallback[B]) extends CasDsl23[A, B] {

    override def mapAsync[C](f: B => AsyncCallback[C]) =
      new CasDsl3[A, C](db, stores, get, eql, prep(_).flatMap(f))

    def set[C](set: TxnDsl[RW] => (A, B) => Txn[RW, C]): AsyncCallback[C] = {
      val txnRO = db.transactionRO(stores: _*)
      val txnRW = db.transactionRW(stores: _*)

      def loopTxn(a: A, b: B): AsyncCallback[Either[A, C]] =
        txnRW { dsl =>
          (get: Txn[RW, A]).flatMap { a2 =>
            if (eql(a, a2))
              set(dsl)(a, b).map(Right(_))
            else
              dsl.pure(Left(a2))
          }
        }

      def loop(a: A): AsyncCallback[Either[A, C]] =
        for {
          b <- prep(a)
          e <- loopTxn(a, b)
        } yield e

      txnRO(_ => get).flatMap(AsyncCallback.tailrec(_)(loop))
    }

    // Convenience methods

    /** Note: insert only */
    def add[K, V](store: ObjectStoreDef.Async[K, V])(key: K, value: V): AsyncCallback[Unit] =
      store.encode(value).flatMap(add(store.sync)(key, _))

    /** Note: insert only */
    def add[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, value: V): AsyncCallback[Unit] =
      setConst(_.objectStore(store).flatMap(_.add(key, value)))

    /** Note: insert only */
    def addResult[K](store: ObjectStoreDef.Async[K, B])(key: K): AsyncCallback[B] =
      addResultBy(store)(key, identityFn)

    /** Note: insert only */
    def addResult[K](store: ObjectStoreDef.Sync[K, B])(key: K): AsyncCallback[B] =
      addResultBy(store)(key, identityFn)

    /** Note: insert only */
    def addResultBy[K, V](store: ObjectStoreDef.Async[K, V])(key: K, f: B => V): AsyncCallback[B] =
      mapAsync { b => store.encode(f(b)).map((b, _)) }
      .addResultBy(store.sync)(key, _._2)
      .map(_._1)

    /** Note: insert only */
    def addResultBy[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, f: B => V): AsyncCallback[B] =
      set(dsl => (_, b) =>
        for {
          s <- dsl.objectStore(store)
          _ <- s.add(key, f(b))
        } yield b
      )

    /** Note: insert only */
    @inline def addResultWhenDefined[K, V](store: ObjectStoreDef.Async[K, V])(key: K)(implicit ev: B => Option[V]): AsyncCallback[B] =
      addResultWhenDefinedBy(store)(key, ev)

    /** Note: insert only */
    @inline def addResultWhenDefined[K, V](store: ObjectStoreDef.Sync[K, V])(key: K)(implicit ev: B => Option[V]): AsyncCallback[B] =
      addResultWhenDefinedBy(store)(key, ev)

    /** Note: insert only */
    def addResultWhenDefinedBy[K, V](store: ObjectStoreDef.Async[K, V])(key: K, f: B => Option[V]): AsyncCallback[B] =
      mapAsync { b => AsyncCallback.traverseOption(f(b))(store.encode(_)).map((b, _)) }
      .addResultWhenDefinedBy(store.sync)(key, _._2)
      .map(_._1)

    /** Note: insert only */
    def addResultWhenDefinedBy[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, f: B => Option[V]): AsyncCallback[B] =
      set(dsl => (_, b) =>
        f(b) match {
          case Some(v) =>
            for {
              s <- dsl.objectStore(store)
              _ <- s.add(key, v)
            } yield b
          case None =>
            dsl.pure(b)
        }
      )

    /** Note: insert only */
    def addWhenDefined[K, V](store: ObjectStoreDef.Async[K, V])(key: K, value: Option[V]): AsyncCallback[Unit] =
      value.fold(AsyncCallback.unit)(add(store)(key, _))

    /** Note: insert only */
    def addWhenDefined[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, value: Option[V]): AsyncCallback[Unit] =
      value.fold(AsyncCallback.unit)(add(store)(key, _))

    def clear[K, V](store: ObjectStoreDef[K, V]): AsyncCallback[Unit] =
      setConst(_.objectStore(store.sync).flatMap(_.clear))

    def delete[K, V](store: ObjectStoreDef[K, V])(key: K): AsyncCallback[Unit] =
      setConst(_.objectStore(store.sync).flatMap(_.delete(key)))

    /** aka upsert */
    def put[K, V](store: ObjectStoreDef.Async[K, V])(key: K, value: V): AsyncCallback[Unit] =
      store.encode(value).flatMap(put(store.sync)(key, _))

    /** aka upsert */
    def put[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, value: V): AsyncCallback[Unit] =
      setConst(_.objectStore(store).flatMap(_.put(key, value)))

    /** aka upsert or delete */
    def putOrDelete[K, V](store: ObjectStoreDef.Async[K, V])(key: K, value: Option[V]): AsyncCallback[Unit] =
      value match {
        case Some(v) => put(store)(key, v)
        case None    => delete(store)(key)
      }

    /** aka upsert or delete */
    def putOrDelete[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, value: Option[V]): AsyncCallback[Unit] =
      value match {
        case Some(v) => put(store)(key, v)
        case None    => delete(store)(key)
      }

    /** Note: upsert */
    @inline def putOrDeleteResult[K, V](store: ObjectStoreDef.Async[K, V])(key: K)(implicit ev: B => Option[V]): AsyncCallback[B] =
      putOrDeleteResultBy(store)(key, ev)

    /** Note: upsert */
    @inline def putOrDeleteResult[K, V](store: ObjectStoreDef.Sync[K, V])(key: K)(implicit ev: B => Option[V]): AsyncCallback[B] =
      putOrDeleteResultBy(store)(key, ev)

    /** Note: upsert */
    def putOrDeleteResultBy[K, V](store: ObjectStoreDef.Async[K, V])(key: K, f: B => Option[V]): AsyncCallback[B] =
      mapAsync { b => AsyncCallback.traverseOption(f(b))(store.encode(_)).map((b, _)) }
      .putOrDeleteResultBy(store.sync)(key, _._2)
      .map(_._1)

    /** Note: upsert */
    def putOrDeleteResultBy[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, f: B => Option[V]): AsyncCallback[B] =
      set(dsl => (_, b) =>
        for {
          s <- dsl.objectStore(store)
          _ <- s.putOrDelete(key, f(b))
        } yield b
      )

    /** Note: upsert */
    def putResult[K](store: ObjectStoreDef.Async[K, B])(key: K): AsyncCallback[B] =
      putResultBy(store)(key, identityFn)

    /** Note: upsert */
    def putResult[K](store: ObjectStoreDef.Sync[K, B])(key: K): AsyncCallback[B] =
      putResultBy(store)(key, identityFn)

    /** Note: upsert */
    def putResultBy[K, V](store: ObjectStoreDef.Async[K, V])(key: K, f: B => V): AsyncCallback[B] =
      mapAsync { b =>
        val v = f(b)
        for {
          enc <- store.encode(v)
        } yield (b, enc)
      }
      .putResultBy(store.sync)(key, _._2)
      .map(_._1)

    /** Note: upsert */
    def putResultBy[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, f: B => V): AsyncCallback[B] =
      set(dsl => (_, b) =>
        for {
          s <- dsl.objectStore(store)
          _ <- s.put(key, f(b))
        } yield b
      )

    /** Note: upsert */
    @inline def putResultWhenDefined[K, V](store: ObjectStoreDef.Async[K, V])(key: K)(implicit ev: B => Option[V]): AsyncCallback[B] =
      putResultWhenDefinedBy(store)(key, ev)

    /** Note: upsert */
    @inline def putResultWhenDefined[K, V](store: ObjectStoreDef.Sync[K, V])(key: K)(implicit ev: B => Option[V]): AsyncCallback[B] =
      putResultWhenDefinedBy(store)(key, ev)

    /** Note: upsert */
    def putResultWhenDefinedBy[K, V](store: ObjectStoreDef.Async[K, V])(key: K, f: B => Option[V]): AsyncCallback[B] =
      mapAsync { b => AsyncCallback.traverseOption(f(b))(store.encode(_)).map((b, _)) }
      .putResultWhenDefinedBy(store.sync)(key, _._2)
      .map(_._1)

    /** Note: upsert */
    def putResultWhenDefinedBy[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, f: B => Option[V]): AsyncCallback[B] =
      set(dsl => (_, b) =>
        f(b) match {
          case Some(v) =>
            for {
              s <- dsl.objectStore(store)
              _ <- s.put(key, v)
            } yield b
          case None =>
            dsl.pure(b)
        }
      )

    /** Note: upsert */
    def putWhenDefined[K, V](store: ObjectStoreDef.Async[K, V])(key: K, value: Option[V]): AsyncCallback[Unit] =
      value.fold(AsyncCallback.unit)(put(store)(key, _))

    /** Note: upsert */
    def putWhenDefined[K, V](store: ObjectStoreDef.Sync[K, V])(key: K, value: Option[V]): AsyncCallback[Unit] =
      value.fold(AsyncCallback.unit)(put(store)(key, _))

    def setConst[C](set: TxnDsl[RW] => Txn[RW, C]): AsyncCallback[C] =
      this.set(dsl => (_, _) => set(dsl))
  }

  // ===================================================================================================================
  private object Internals {

    val success_ = Success(())

    def asyncRequest_[R <: IDBRequest[Any, _]](act: => R): AsyncCallback[Unit] =
      asyncRequest(act)(_ => ())

    def asyncRequest[R <: IDBRequest[Any, _], A](act: => R)(onSuccess: R => A): AsyncCallback[A] =
      AsyncCallback.promise[A].asAsyncCallback.flatMap { case (promise, complete) =>
        val raw = act

        raw.onerror = event => {
          complete(Failure(Error(event))).runNow()
        }

        raw.onsuccess = _ => {
          complete(Try(onSuccess(raw))).runNow()
        }

        promise
      }

    def versionChange(db: DatabaseInVersionChange, e: IDBVersionChangeEvent): VersionChange =
      VersionChange(db, e.oldVersion.toInt, e.newVersionOption.map(_.toInt))

    def mkStoreArray(stores: Seq[ObjectStoreDef[_, _]]): js.Array[String] = {
      val a = new js.Array[String]
      stores.foreach(s => a.push(s.name))
      a
    }

    def interpretTxn[M <: TxnMode, A](txn: IDBTransaction, dsl: Txn[M, A]): AsyncCallback[A] =
      AsyncCallback.suspend {
        import TxnStep._

        val stores = js.Dynamic.literal().asInstanceOf[js.Dictionary[IDBObjectStore]]

        def getStore(s: ObjectStore[_, _]) =
          AsyncCallback.delay(stores.get(s.defn.name).get)

        def interpret[B](step: TxnStep[TxnMode, B]): AsyncCallback[B] = {
          step match {

            case FlatMap(fa, f) =>
              interpret(fa).flatMap(a => interpret(f(a)))

            case StoreGet(s, k) =>
              getStore(s).flatMap { store =>
                asyncRequest(store.get(k.asJs))(_.result).flatMapSync { result =>
                  if (js.isUndefined(result))
                    CallbackTo.pure(None)
                  else
                    s.defn.valueCodec.decode(result).map(Some(_))
                }
              }

            case Eval(c) =>
              c.asAsyncCallback

            case Suspend(s) =>
              s.asAsyncCallback.flatMap(interpret(_))

            case GetStore(sd) =>
              AsyncCallback.delay {
                val s = txn.objectStore(sd.name)
                stores.put(sd.name, s)
                new ObjectStore(sd)
              }

            case StoreAdd(s, k, v) =>
              getStore(s).flatMap { store =>
                asyncRequest_(store.add(v, k.asJs))
              }

            case StorePut(s, k, v) =>
              getStore(s).flatMap { store =>
                asyncRequest_(store.put(v, k.asJs))
              }

            case Map(fa, f) =>
              interpret(fa).map(f)

            case StoreDelete(s, k) =>
              getStore(s).flatMap { store =>
                asyncRequest_(store.delete(k.asJs))
              }

            case StoreGetAllKeys(s) =>
              import s.defn.keyCodec
              getStore(s).flatMap { store =>
                asyncRequest(store.getAllKeys()) { req =>
                  val rawKeys = req.result
                  Vector.tabulate(rawKeys.length) { i =>
                    val rawKey = rawKeys(i)
                    keyCodec.decode(IndexedDbKey.fromJs(rawKey)).runNow() // safe in asyncRequest onSuccess
                  }
                }
              }

            case StoreGetAllVals(s) =>
              import s.defn.valueCodec
              getStore(s).flatMap { store =>
                asyncRequest(store.getAll()) { req =>
                  val rawVals = req.result
                  Vector.tabulate(rawVals.length) { i =>
                    val rawVal = rawVals(i)
                    valueCodec.decode(rawVal).runNow() // safe in asyncRequest onSuccess
                  }
                }
              }

            case StoreClear(s) =>
              getStore(s).flatMap { store =>
                asyncRequest_(store.clear())
              }

            case TailRec(z, f) =>
              AsyncCallback.tailrec(z)(a => interpret(f(a)))

          }
        }

        interpret(dsl.step)
      }

  } // object Internals
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy