Skip to content

Commit

Permalink
Better separation of Model and Collection and listener model
Browse files Browse the repository at this point in the history
  • Loading branch information
darkfrog26 committed May 10, 2024
1 parent 67f89ed commit 82a5efc
Show file tree
Hide file tree
Showing 27 changed files with 233 additions and 116 deletions.
2 changes: 1 addition & 1 deletion all/src/test/scala/spec/SimpleHaloAndLuceneSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class SimpleHaloAndLuceneSpec extends AsyncWordSpec with AsyncIOSpec with Matche
}
"delete John" in {
Person.delete(id1).map { deleted =>
deleted should not be empty
deleted should be(id1)
}
}
"verify exactly one object in data" in {
Expand Down
2 changes: 1 addition & 1 deletion all/src/test/scala/spec/SimpleHaloAndSQLiteSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ class SimpleHaloAndSQLiteSpec extends AsyncWordSpec with AsyncIOSpec with Matche
}
"delete John" in {
Person.delete(id1).map { deleted =>
deleted should not be empty
deleted should be(id1)
}
}
"verify exactly one object in data" in {
Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ val developerURL: String = "https://matthicks.com"

name := projectName
ThisBuild / organization := org
ThisBuild / version := "0.5.0"
ThisBuild / version := "0.6.0-SNAPSHOT"
ThisBuild / scalaVersion := scala213
ThisBuild / crossScalaVersions := allScalaVersions
ThisBuild / scalacOptions ++= Seq("-unchecked", "-deprecation")
Expand Down
33 changes: 20 additions & 13 deletions core/src/main/scala/lightdb/index/IndexSupport.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
package lightdb.index

import cats.effect.IO
import lightdb.model.{AbstractCollection, Collection}
import lightdb.model.{AbstractCollection, Collection, DocumentAction, DocumentListener, DocumentModel}
import lightdb.query.{PagedResults, Query, SearchContext}
import lightdb.Document

trait IndexSupport[D <: Document[D]] extends Collection[D] {
lazy val query: Query[D] = Query(this)
trait IndexSupport[D <: Document[D]] extends DocumentModel[D] {
private var _collection: Option[AbstractCollection[D]] = None
protected def collection: AbstractCollection[D] = this match {
case c: AbstractCollection[_] => c.asInstanceOf[AbstractCollection[D]]
case _ => _collection.getOrElse(throw new RuntimeException("DocumentModel not initialized with Collection (yet)"))
}

override def commit(): IO[Unit] = super.commit().flatMap(_ => index.commit())
lazy val query: Query[D] = Query(this, collection)

override protected[lightdb] def initModel(collection: AbstractCollection[D]): Unit = {
super.initModel(collection)
_collection = Some(collection)
collection.commitActions += index.commit()
collection.postSet += ((action: DocumentAction, doc: D, collection: AbstractCollection[D]) => {
indexDoc(doc, index.fields).map(_ => Some(doc))
})
collection.postDelete += ((action: DocumentAction, doc: D, collection: AbstractCollection[D]) => {
index.delete(doc._id).map(_ => Some(doc))
})
}

def index: Indexer[D]

Expand All @@ -19,14 +35,5 @@ trait IndexSupport[D <: Document[D]] extends Collection[D] {
offset: Int,
after: Option[PagedResults[D]]): IO[PagedResults[D]]

override def postSet(doc: D, collection: AbstractCollection[D]): IO[Unit] = for {
_ <- indexDoc(doc, index.fields)
_ <- super.postSet(doc, collection)
} yield ()

override def postDelete(doc: D, collection: AbstractCollection[D]): IO[Unit] = index.delete(doc._id).flatMap { _ =>
super.postDelete(doc, collection)
}

protected def indexDoc(doc: D, fields: List[IndexedField[_, D]]): IO[Unit]
}
6 changes: 3 additions & 3 deletions core/src/main/scala/lightdb/index/IndexedField.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ package lightdb.index
import fabric.rw.{Convertible, RW}
import fabric.{Json, Null}
import lightdb.Document
import lightdb.model.Collection
import lightdb.model.{AbstractCollection, Collection}

trait IndexedField[F, D <: Document[D]] {
implicit def rw: RW[F]

def fieldName: String
def collection: Collection[D]
def indexSupport: IndexSupport[D]
def get: D => Option[F]
def getJson: D => Json = (doc: D) => get(doc) match {
case Some(value) => value.json
case None => Null
}

collection.asInstanceOf[IndexSupport[D]].index.register(this)
indexSupport.index.register(this)
}
51 changes: 27 additions & 24 deletions core/src/main/scala/lightdb/model/AbstractCollection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package lightdb.model
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import cats.implicits._
import fabric.rw.RW
import io.chrisdavenport.keysemaphore.KeySemaphore
import lightdb.{DocLock, Document, Id, IndexedLinks, LightDB, MaxLinks, Store}

trait AbstractCollection[D <: Document[D]] {
trait AbstractCollection[D <: Document[D]] extends DocumentActionSupport[D] {
// Id-level locking
private lazy val sem = KeySemaphore.of[IO, Id[D]](_ => 1L).unsafeRunSync()

implicit val rw: RW[D]

def model: DocumentModel[D]

def collectionName: String
Expand All @@ -22,9 +25,11 @@ trait AbstractCollection[D <: Document[D]] {

protected lazy val store: Store = db.createStoreInternal(collectionName)

model.initModel(this)

def idStream: fs2.Stream[IO, Id[D]] = store.keyStream

def stream: fs2.Stream[IO, D] = store.streamJsonDocs[D](model.rw)
def stream: fs2.Stream[IO, D] = store.streamJsonDocs[D](rw)

def withLock[Return](id: Id[D])(f: DocLock[D] => IO[Return])
(implicit existingLock: DocLock[D] = new DocLock.Empty[D]): IO[Return] = {
Expand All @@ -45,13 +50,12 @@ trait AbstractCollection[D <: Document[D]] {
}
}

def set(doc: D)(implicit existingLock: DocLock[D] = new DocLock.Empty[D]): IO[D] = withLock(doc._id) { _ =>
for {
modified <- model.preSet(doc, this)
json <- model.preSetJson(model.rw.read(doc), this)
_ <- store.putJson(doc._id, json)
_ <- model.postSet(doc, this)
} yield modified
def set(doc: D)(implicit existingLock: DocLock[D] = new DocLock.Empty[D]): IO[D] = withLock(doc._id) { lock =>
doSet(
doc = doc,
collection = this,
set = (id, json) => store.putJson(id, json)
)(lock)
}

def modify(id: Id[D])
Expand All @@ -66,18 +70,13 @@ trait AbstractCollection[D <: Document[D]] {
}

def delete(id: Id[D])
(implicit existingLock: DocLock[D] = new DocLock.Empty[D]): IO[Option[D]] = withLock(id) { implicit lock =>
for {
modifiedId <- model.preDelete(id, this)
deleted <- get(modifiedId).flatMap {
case Some(d) => store.delete(id).map(_ => Some(d))
case None => IO.pure(None)
}
_ <- deleted match {
case Some(doc) => model.postDelete(doc, this)
case None => IO.unit
}
} yield deleted
(implicit existingLock: DocLock[D] = new DocLock.Empty[D]): IO[Id[D]] = withLock(id) { implicit lock =>
doDelete(
id = id,
collection = this,
get = apply,
delete = id => store.delete(id)
)(lock)
}

def truncate(): IO[Unit] = for {
Expand All @@ -86,16 +85,20 @@ trait AbstractCollection[D <: Document[D]] {
_ <- commit().whenA(autoCommit)
} yield ()

def get(id: Id[D]): IO[Option[D]] = store.getJsonDoc(id)(model.rw)
def get(id: Id[D]): IO[Option[D]] = store.getJsonDoc(id)(rw)

def apply(id: Id[D]): IO[D] = get(id)
.map(_.getOrElse(throw new RuntimeException(s"$id not found in $collectionName")))

def size: IO[Int] = store.size

def commit(): IO[Unit] = store.commit()
def commit(): IO[Unit] = store.commit().flatMap { _ =>
commitActions.invoke()
}

def dispose(): IO[Unit] = IO.unit
def dispose(): IO[Unit] = store.dispose().flatMap { _ =>
disposeActions.invoke()
}

/**
* Creates a key/value stored object with a list of links. This can be incredibly efficient for small lists, but much
Expand Down
13 changes: 13 additions & 0 deletions core/src/main/scala/lightdb/model/DocumentAction.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package lightdb.model

sealed trait DocumentAction

object DocumentAction {
case object PreSet extends DocumentAction

case object PostSet extends DocumentAction

case object PreDelete extends DocumentAction

case object PostDelete extends DocumentAction
}
45 changes: 45 additions & 0 deletions core/src/main/scala/lightdb/model/DocumentActionSupport.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package lightdb.model

import cats.effect.IO
import fabric.Json
import lightdb.{DocLock, Document, Id}

trait DocumentActionSupport[D <: Document[D]] {
val preSet: DocumentActions[D, D] = DocumentActions[D, D](DocumentAction.PreSet)
val preSetJson: DocumentActions[D, Json] = DocumentActions[D, Json](DocumentAction.PreSet)
val postSet: DocumentActions[D, D] = DocumentActions[D, D](DocumentAction.PostSet)

val preDeleteId: DocumentActions[D, Id[D]] = DocumentActions[D, Id[D]](DocumentAction.PreDelete)
val preDelete: DocumentActions[D, D] = DocumentActions[D, D](DocumentAction.PreDelete)
val postDelete: DocumentActions[D, D] = DocumentActions[D, D](DocumentAction.PostDelete)

val commitActions: UnitActions = new UnitActions
val disposeActions: UnitActions = new UnitActions

protected def doSet(doc: D,
collection: AbstractCollection[D],
set: (Id[D], Json) => IO[Unit])
(implicit lock: DocLock[D]): IO[D] = preSet.invoke(doc, collection).flatMap {
case Some(doc) => preSetJson.invoke(collection.rw.read(doc), collection).flatMap {
case Some(json) => set(doc._id, json).flatMap { _ =>
postSet.invoke(doc, collection).map(_ => doc)
}
case None => IO.pure(doc)
}
case None => IO.pure(doc)
}

protected def doDelete(id: Id[D],
collection: AbstractCollection[D],
get: Id[D] => IO[D],
delete: Id[D] => IO[Unit])
(implicit lock: DocLock[D]): IO[Id[D]] = preDeleteId.invoke(id, collection).flatMap {
case Some(id) => get(id).flatMap(doc => preDelete.invoke(doc, collection).flatMap {
case Some(doc) => delete(doc._id).flatMap { _ =>
postDelete.invoke(doc, collection).map(_ => doc._id)
}
case None => IO.pure(id)
})
case None => IO.pure(id)
}
}
27 changes: 27 additions & 0 deletions core/src/main/scala/lightdb/model/DocumentActions.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package lightdb.model

import cats.effect.IO
import lightdb.Document

case class DocumentActions[D <: Document[D], Value](action: DocumentAction) {
private var list = List.empty[DocumentListener[D, Value]]

def +=(listener: DocumentListener[D, Value]): Unit = add(listener)

def add(listener: DocumentListener[D, Value]): Unit = synchronized {
list = list ::: List(listener)
}

private[model] def invoke(value: Value, collection: AbstractCollection[D]): IO[Option[Value]] = {
def recurse(value: Value, list: List[DocumentListener[D, Value]]): IO[Option[Value]] = if (list.isEmpty) {
IO.pure(Some(value))
} else {
list.head(action, value, collection).flatMap {
case Some(v) => recurse(v, list.tail)
case None => IO.pure(None)
}
}

recurse(value, list)
}
}
8 changes: 8 additions & 0 deletions core/src/main/scala/lightdb/model/DocumentListener.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package lightdb.model

import cats.effect.IO
import lightdb.Document

trait DocumentListener[D <: Document[D], Value] {
def apply(action: DocumentAction, value: Value, collection: AbstractCollection[D]): IO[Option[Value]]
}
44 changes: 16 additions & 28 deletions core/src/main/scala/lightdb/model/DocumentModel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,24 @@ import lightdb.{Document, Id, IndexedLinks}
trait DocumentModel[D <: Document[D]] {
type Field[F] = IndexedField[F, D]

implicit val rw: RW[D]

private[lightdb] var _indexedLinks = List.empty[IndexedLinks[_, D]]

def indexedLinks: List[IndexedLinks[_, D]] = _indexedLinks

/**
* Called before preSetJson and before the data is set to the database
*/
def preSet(doc: D, collection: AbstractCollection[D]): IO[D] = IO.pure(doc)

/**
* Called after preSet and before the data is set to the database
*/
def preSetJson(json: Json, collection: AbstractCollection[D]): IO[Json] = IO.pure(json)

/**
* Called after set
*/
def postSet(doc: D, collection: AbstractCollection[D]): IO[Unit] = for {
// Update IndexedLinks
_ <- _indexedLinks.map(_.add(doc)).sequence
_ <- collection.commit().whenA(collection.autoCommit)
} yield ()

def preDelete(id: Id[D], collection: AbstractCollection[D]): IO[Id[D]] = IO.pure(id)

def postDelete(doc: D, collection: AbstractCollection[D]): IO[Unit] = for {
// Update IndexedLinks
_ <- _indexedLinks.map(_.remove(doc)).sequence
_ <- collection.commit().whenA(collection.autoCommit)
} yield ()
protected[lightdb] def initModel(collection: AbstractCollection[D]): Unit = {
collection.postSet.add((action: DocumentAction, doc: D, collection: AbstractCollection[D]) => {
for {
// Add to IndexedLinks
_ <- _indexedLinks.map(_.add(doc)).sequence
_ <- collection.commit().whenA(collection.autoCommit)
} yield Some(doc)
})
collection.postDelete.add((action: DocumentAction, doc: D, collection: AbstractCollection[D]) => {
for {
// Remove from IndexedLinks
_ <- _indexedLinks.map(_.remove(doc)).sequence
_ <- collection.commit().whenA(collection.autoCommit)
} yield Some(doc)
})
}
}
11 changes: 7 additions & 4 deletions core/src/main/scala/lightdb/model/RecordDocumentModel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import fabric._
import lightdb.RecordDocument

trait RecordDocumentModel[D <: RecordDocument[D]] extends DocumentModel[D] {
override def preSetJson(json: Json, collection: AbstractCollection[D]): IO[Json] = IO {
json.modify("modified") { _ =>
System.currentTimeMillis()
}
override protected[lightdb] def initModel(collection: AbstractCollection[D]): Unit = {
super.initModel(collection)
collection.preSetJson.add(new DocumentListener[D, Json] {
override def apply(action: DocumentAction, json: Json, collection: AbstractCollection[D]): IO[Option[Json]] = IO {
Some(json.modify("modified")(_ => System.currentTimeMillis()))
}
})
}
}
15 changes: 15 additions & 0 deletions core/src/main/scala/lightdb/model/UnitActions.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package lightdb.model

import cats.effect.IO

class UnitActions {
private var list = List.empty[() => IO[Unit]]

def +=(listener: => IO[Unit]): Unit = add(listener)

def add(listener: => IO[Unit]): Unit = synchronized {
list = list ::: List(() => listener)
}

private[model] def invoke(): IO[Unit] = list.map(_()).sequence.map(_ => ())
}
Loading

0 comments on commit 82a5efc

Please sign in to comment.