feat: initial commit
This commit is contained in:
commit
f9ac85dfe4
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
/bower_components/
|
||||
/node_modules/
|
||||
/.pulp-cache/
|
||||
/output/
|
||||
/generated-docs/
|
||||
/.psc-package/
|
||||
/.psc*
|
||||
/.purs*
|
||||
/.psa*
|
||||
/.spago
|
||||
.log
|
||||
.purs-repl
|
||||
.env
|
8
.prettierrc.cjs
Normal file
8
.prettierrc.cjs
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
tabWidth: 2,
|
||||
trailingComma: 'all',
|
||||
singleQuote: true,
|
||||
semi: false,
|
||||
arrowParens: 'avoid',
|
||||
plugins: [],
|
||||
}
|
3
.tool-versions
Normal file
3
.tool-versions
Normal file
@ -0,0 +1,3 @@
|
||||
bun 1.0.3
|
||||
purescript 0.15.12
|
||||
nodejs 20.8.0
|
28
bun/fmt.js
Normal file
28
bun/fmt.js
Normal file
@ -0,0 +1,28 @@
|
||||
/** @type {(parser: string, ps: string[]) => import("bun").Subprocess} */
|
||||
const prettier = (parser, ps) =>
|
||||
Bun.spawn(['bun', 'x', 'prettier', '--write', '--parser', parser, ...ps], {
|
||||
stdout: 'inherit',
|
||||
stderr: 'inherit',
|
||||
})
|
||||
|
||||
const procs = [
|
||||
prettier('babel', ['./src/**/*.js', './bun/**/*.js', './.prettierrc.cjs']),
|
||||
prettier('json', ['./package.json', './jsconfig.json']),
|
||||
prettier('sh', ['./Dockerfile']),
|
||||
Bun.spawn(
|
||||
[
|
||||
'bun',
|
||||
'x',
|
||||
'purs-tidy',
|
||||
'format-in-place',
|
||||
'src/**/*.purs',
|
||||
'test/**/*.purs',
|
||||
],
|
||||
{
|
||||
stdout: 'inherit',
|
||||
stderr: 'inherit',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
await Promise.all(procs.map(p => p.exited))
|
16
jsconfig.json
Normal file
16
jsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["bun-types"],
|
||||
"lib": ["esnext"],
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"noEmit": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["tmp.js", "src/**/*.js", "bun/**/*.js"]
|
||||
}
|
18
package.json
Normal file
18
package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@ts/scraper",
|
||||
"private": true,
|
||||
"module": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"fmt": "bun bun/fmt.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest",
|
||||
"purs-tidy": "^0.10.0",
|
||||
"spago": "next"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
15
spago.yaml
Normal file
15
spago.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
package:
|
||||
dependencies:
|
||||
- aff
|
||||
- arrays
|
||||
- avar
|
||||
- console
|
||||
name: sync
|
||||
test:
|
||||
main: Test.Main
|
||||
dependencies:
|
||||
- spec
|
||||
workspace:
|
||||
package_set:
|
||||
url: https://raw.githubusercontent.com/purescript/package-sets/psc-0.15.10-20230930/packages.json
|
||||
hash: sha256-nTsd44o7/hrTdk0c6dh0wyBqhFFDJJIeKdQU6L1zv/A=
|
112
src/Control.Monad.Async.State.purs
Normal file
112
src/Control.Monad.Async.State.purs
Normal file
@ -0,0 +1,112 @@
|
||||
module Control.Monad.State.Async where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Control.Alt (class Alt)
|
||||
import Control.Alternative (class Alternative, class Plus)
|
||||
import Control.Monad.Error.Class (class MonadError, class MonadThrow, liftEither, try)
|
||||
import Control.Monad.Fork.Class (class MonadBracket, class MonadFork, class MonadKill, bracket, kill, never, uninterruptible)
|
||||
import Control.Monad.Reader (class MonadAsk, ReaderT(..), ask)
|
||||
import Control.Monad.Trans.Class (class MonadTrans, lift)
|
||||
import Control.Parallel (class Parallel, parallel, sequential)
|
||||
import Data.Async.Class (class AsyncState)
|
||||
import Data.Async.Class as State
|
||||
import Data.Async.Mutex (Mutex(..))
|
||||
import Data.Async.RwLock (RwLock(..))
|
||||
import Data.Maybe (Maybe)
|
||||
import Data.Newtype (class Newtype, unwrap, wrap)
|
||||
import Data.Traversable (for, for_)
|
||||
import Data.Tuple (fst, snd)
|
||||
import Data.Tuple.Nested (type (/\), (/\))
|
||||
import Effect.Aff.Class (class MonadAff)
|
||||
import Effect.Class (class MonadEffect)
|
||||
|
||||
-- | A monadic context with some mutable state `s`
|
||||
-- | that is in a concurrency-safe `AsyncState` wrapper `w`
|
||||
-- |
|
||||
-- | Supports the following operations:
|
||||
-- | - `asyncTryRead` - Read the state as it is now, returning `Nothing` if the resource is currently locked
|
||||
-- | - `asyncRead` - Read the state, blocking if the resource is locked
|
||||
-- | - `asyncWrite` - Map the state's value, blocking if the resource is locked
|
||||
-- | - `ask` - Direct access to the low-level `AsyncState`
|
||||
class MonadAsyncState :: (Type -> Type) -> (Type -> Type) -> (Type -> Type) -> Type -> (Type -> Type) -> ((Type -> Type) -> Type -> Type) -> Constraint
|
||||
class (MonadTrans t, MonadAff (t m), MonadAff m, AsyncState w wwl wrl, MonadAsk (w s) (t m)) <= MonadAsyncState w wwl wrl s m t | w -> wwl, w -> wrl, m -> w, m -> s, t -> m
|
||||
|
||||
asyncTryRead :: forall w wl rl s m t. MonadAsyncState w wl rl s m t => t m (Maybe s)
|
||||
asyncTryRead = do
|
||||
state <- ask
|
||||
readLock :: Maybe (rl s) <- State.tryLock state
|
||||
snap <- for readLock (State.read state)
|
||||
for_ readLock (State.unlock state)
|
||||
pure snap
|
||||
|
||||
asyncWrite :: forall w wl rl s m t a e. MonadError e (t m) => MonadAsyncState w wl rl s m t => (s -> m (a /\ s)) -> t m a
|
||||
asyncWrite f = do
|
||||
state <- ask
|
||||
lock :: wl s <- State.lock state
|
||||
s <- State.read state lock
|
||||
e <- try $ lift $ f s
|
||||
for_ e (State.write state lock <<< snd)
|
||||
State.unlock state lock
|
||||
fst <$> liftEither e
|
||||
|
||||
asyncModify :: forall w wl rl s m t e. MonadError e (t m) => MonadAsyncState w wl rl s m t => (s -> m s) -> t m Unit
|
||||
asyncModify f = asyncWrite (map (unit /\ _) <<< f)
|
||||
|
||||
asyncRead :: forall w wl rl s m t a e. MonadError e (t m) => MonadAsyncState w wl rl s m t => (s -> m a) -> t m a
|
||||
asyncRead f = do
|
||||
state <- ask
|
||||
lock :: rl s <- State.lock state
|
||||
s <- State.read state lock
|
||||
e <- try $ lift $ f s
|
||||
State.unlock state lock
|
||||
liftEither e
|
||||
|
||||
asyncPut :: forall w wl rl s m t e. MonadError e (t m) => MonadAsyncState w wl rl s m t => s -> t m Unit
|
||||
asyncPut = asyncWrite <<< const <<< pure <<< (unit /\ _)
|
||||
|
||||
-- | Implementor of `MonadAsyncState` usable with any `AsyncState` structure.
|
||||
newtype AsyncStateT :: (Type -> Type) -> Type -> (Type -> Type) -> Type -> Type
|
||||
newtype AsyncStateT w s m ma = AsyncStateT (ReaderT (w s) m ma)
|
||||
|
||||
derive instance Newtype (AsyncStateT w s m ma) _
|
||||
derive newtype instance MonadTrans (AsyncStateT w s)
|
||||
derive newtype instance (Alternative m) => Alternative (AsyncStateT w s m)
|
||||
derive newtype instance (Alt m) => Alt (AsyncStateT w s m)
|
||||
derive newtype instance (Plus m) => Plus (AsyncStateT w s m)
|
||||
derive newtype instance (Applicative m) => Applicative (AsyncStateT w s m)
|
||||
derive newtype instance (Apply m) => Apply (AsyncStateT w s m)
|
||||
derive newtype instance (Bind m) => Bind (AsyncStateT w s m)
|
||||
derive newtype instance (Functor m) => Functor (AsyncStateT w s m)
|
||||
derive newtype instance (Monad m) => Monad (AsyncStateT w s m)
|
||||
derive newtype instance (Monad m) => MonadAsk (w s) (AsyncStateT w s m)
|
||||
derive newtype instance (MonadEffect m) => MonadEffect (AsyncStateT w s m)
|
||||
derive newtype instance (MonadAff m) => MonadAff (AsyncStateT w s m)
|
||||
derive newtype instance (MonadThrow e m) => MonadThrow e (AsyncStateT w s m)
|
||||
derive newtype instance (MonadError e m) => MonadError e (AsyncStateT w s m)
|
||||
derive newtype instance (MonadFork f m) => MonadFork f (AsyncStateT w s m)
|
||||
instance (Parallel f m) => Parallel (AsyncStateT w s f) (AsyncStateT w s m) where
|
||||
parallel a = wrap $ parallel $ unwrap a
|
||||
sequential a = wrap $ sequential $ unwrap a
|
||||
|
||||
instance (MonadError e m, MonadKill e f m) => MonadKill e f (AsyncStateT w s m) where
|
||||
kill e f = wrap $ kill e f
|
||||
|
||||
instance (MonadBracket e f m) => MonadBracket e f (AsyncStateT w s m) where
|
||||
bracket a f c = wrap $ bracket (unwrap a) (\fa fb -> unwrap $ f fa fb) (unwrap <<< c)
|
||||
uninterruptible a = wrap $ uninterruptible $ unwrap a
|
||||
never = wrap never
|
||||
|
||||
instance (AsyncState w wl rl, MonadAff m) => MonadAsyncState w wl rl s m (AsyncStateT w s)
|
||||
|
||||
type RwLockStateT s m = AsyncStateT RwLock s m
|
||||
type MutexStateT s m = AsyncStateT Mutex s m
|
||||
|
||||
runAsyncState :: forall w wl rl s m ma. MonadAff m => AsyncState w wl rl => s -> AsyncStateT w s m ma -> m ma
|
||||
runAsyncState s (AsyncStateT (ReaderT f)) = f =<< State.boxed s
|
||||
|
||||
runMutexState :: forall s m ma. MonadAff m => s -> MutexStateT s m ma -> m ma
|
||||
runMutexState s (AsyncStateT (ReaderT f)) = f =<< State.boxed s
|
||||
|
||||
runRwLockState :: forall s m ma. MonadAff m => s -> RwLockStateT s m ma -> m ma
|
||||
runRwLockState s (AsyncStateT (ReaderT f)) = f =<< State.boxed s
|
57
src/Data.Async.Class.purs
Normal file
57
src/Data.Async.Class.purs
Normal file
@ -0,0 +1,57 @@
|
||||
module Data.Async.Class where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Data.Maybe (Maybe)
|
||||
import Effect.Aff.Class (class MonadAff)
|
||||
|
||||
-- | Acquire a lock that guarantees access to mutable state
|
||||
-- |
|
||||
-- | Care must be taken to release locks when errors are raised
|
||||
-- | to not cause deadlocks.
|
||||
-- |
|
||||
-- | Consider using a high-level structure like `AsyncStateT`,
|
||||
-- | which will automatically acquire and release locks when errors
|
||||
-- | occur.
|
||||
class AsyncStateLock :: (Type -> Type) -> (Type -> Type) -> Constraint
|
||||
class AsyncStateLock m lock | lock -> m where
|
||||
-- | Attempt to synchronously acquire a lock, returning `Nothing`
|
||||
-- | if a call to `lock` would have blocked.
|
||||
tryLock :: forall async a. MonadAff async => m a -> async (Maybe (lock a))
|
||||
|
||||
-- | Block the current `Aff`-like computation until a lock may
|
||||
-- | be acquired.
|
||||
lock :: forall async a. MonadAff async => m a -> async (lock a)
|
||||
|
||||
-- | Release the lock, unblocking other threads.
|
||||
-- |
|
||||
-- | Repeated calls to `unlock` or calls to `unlock` on the wrong
|
||||
-- | AsyncState `m` may either silently fail or raise an error.
|
||||
-- |
|
||||
-- | Implementations must raise an error if using a lock is attempted after
|
||||
-- | `unlock` has been invoked.
|
||||
unlock :: forall async a. MonadAff async => m a -> lock a -> async Unit
|
||||
|
||||
-- | A lock type that supports writing data to the mutable state.
|
||||
-- |
|
||||
-- | Writable locks must also be Readable.
|
||||
class AsyncStateWritable :: (Type -> Type) -> (Type -> Type) -> Constraint
|
||||
class (AsyncStateReadable m lock) <= AsyncStateWritable m lock | lock -> m where
|
||||
-- | Write a new value into `m`
|
||||
write :: forall a async. MonadAff async => m a -> lock a -> a -> async Unit
|
||||
|
||||
-- | A lock type that supports reading data from the mutable state
|
||||
class AsyncStateReadable :: (Type -> Type) -> (Type -> Type) -> Constraint
|
||||
class AsyncStateReadable m lock | lock -> m where
|
||||
-- | Read the current value in `m`
|
||||
read :: forall a async. MonadAff async => m a -> lock a -> async a
|
||||
|
||||
-- | Mutable state that supports concurrent access.
|
||||
-- |
|
||||
-- | Implementors may use separate read & write guard types
|
||||
-- | (ex. `RwLock`) or use a single guard type for both reading
|
||||
-- | and writing (ex. `Mutex`)
|
||||
class AsyncState :: (Type -> Type) -> (Type -> Type) -> (Type -> Type) -> Constraint
|
||||
class (AsyncStateReadable m readLock, AsyncStateReadable m writeLock, AsyncStateWritable m writeLock, AsyncStateLock m writeLock, AsyncStateLock m readLock) <= AsyncState m writeLock readLock | writeLock -> m, readLock -> m, m -> readLock, m -> writeLock where
|
||||
-- | Create an `m` with initial value `a`
|
||||
boxed :: forall async a. MonadAff async => a -> async (m a)
|
46
src/Data.Async.Mutex.purs
Normal file
46
src/Data.Async.Mutex.purs
Normal file
@ -0,0 +1,46 @@
|
||||
module Data.Async.Mutex (Mutex, MutexGuard) where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Control.Monad.Error.Class (liftMaybe, throwError)
|
||||
import Data.Async.Class (class AsyncState, class AsyncStateLock, class AsyncStateReadable, class AsyncStateWritable, read)
|
||||
import Data.Maybe (isNothing)
|
||||
import Data.Traversable (for, for_, traverse)
|
||||
import Effect.Aff.AVar (AVar)
|
||||
import Effect.Aff.AVar as AVar
|
||||
import Effect.Aff.Class (liftAff)
|
||||
import Effect.Class (liftEffect)
|
||||
import Effect.Console as Console
|
||||
import Effect.Exception (error)
|
||||
|
||||
-- | A lock guaranteeing exclusive access to
|
||||
-- | the data contained within a `Mutex`
|
||||
data MutexGuard a = MutexGuard (AVar a)
|
||||
|
||||
-- | Mutable state guaranteeing mutually exclusive
|
||||
-- | access to the data of type `a`.
|
||||
-- |
|
||||
-- | There will be at most 1 thread with access to
|
||||
-- | the data contained in the mutex at a time.
|
||||
data Mutex a = Mutex (AVar a)
|
||||
|
||||
instance AsyncState Mutex MutexGuard MutexGuard where
|
||||
boxed = liftAff <<< map Mutex <<< AVar.new
|
||||
|
||||
instance AsyncStateReadable Mutex MutexGuard where
|
||||
read _ (MutexGuard cell) = liftAff $ liftMaybe (error "MutexGuard used after `unlock`!") =<< AVar.tryRead cell
|
||||
|
||||
instance AsyncStateWritable Mutex MutexGuard where
|
||||
write _ (MutexGuard cell) s = liftAff $ (const $ AVar.put s cell) =<< liftMaybe (error "MutexGuard used after `unlock`!") =<< AVar.tryTake cell
|
||||
|
||||
instance AsyncStateLock Mutex MutexGuard where
|
||||
unlock (Mutex stateCell) (MutexGuard localStateCell) = liftAff do
|
||||
state <- AVar.tryTake localStateCell
|
||||
when (isNothing state) $ throwError $ error "MutexGuard unlocked already!"
|
||||
for_ state (flip AVar.put stateCell)
|
||||
lock (Mutex stateCell) = liftAff do
|
||||
state <- AVar.take stateCell
|
||||
MutexGuard <$> AVar.new state
|
||||
tryLock (Mutex stateCell) = liftAff do
|
||||
state <- AVar.tryTake stateCell
|
||||
for state (map MutexGuard <<< AVar.new)
|
136
src/Data.Async.RwLock.purs
Normal file
136
src/Data.Async.RwLock.purs
Normal file
@ -0,0 +1,136 @@
|
||||
module Data.Async.RwLock (RwLock, ReadGuard, WriteGuard) where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Control.Monad.Error.Class (liftMaybe, throwError)
|
||||
import Data.Array as Array
|
||||
import Data.Async.Class (class AsyncState, class AsyncStateLock, class AsyncStateReadable, class AsyncStateWritable, lock)
|
||||
import Data.Either (Either(..))
|
||||
import Data.Maybe (Maybe(..), fromMaybe)
|
||||
import Data.Newtype (class Newtype, wrap)
|
||||
import Data.Set (Set)
|
||||
import Data.Set as Set
|
||||
import Data.Traversable (for, for_)
|
||||
import Effect (Effect)
|
||||
import Effect.Aff (Aff, launchAff_, makeAff)
|
||||
import Effect.Aff.AVar (AVar)
|
||||
import Effect.Aff.AVar as AVar
|
||||
import Effect.Aff.Class (liftAff)
|
||||
import Effect.Class (liftEffect)
|
||||
import Effect.Exception (error)
|
||||
|
||||
data ReadGuard :: Type -> Type
|
||||
data ReadGuard a = ReadGuard ReaderId a
|
||||
|
||||
data WriteGuard :: Type -> Type
|
||||
data WriteGuard a = WriteGuard (AVar a)
|
||||
|
||||
newtype ReaderId = ReaderId Int
|
||||
|
||||
derive instance Newtype ReaderId _
|
||||
derive newtype instance Semiring ReaderId
|
||||
derive newtype instance Show ReaderId
|
||||
derive newtype instance Eq ReaderId
|
||||
derive newtype instance Ord ReaderId
|
||||
|
||||
newtype ReaderCount = ReaderCount Int
|
||||
|
||||
derive instance Newtype ReaderCount _
|
||||
|
||||
-- | A mutable location in memory with guaranteed exclusive access to
|
||||
-- | any number of readers or a single writer
|
||||
data RwLock a = RwLock
|
||||
{ nextReaderId :: AVar ReaderId
|
||||
, readers :: AVar (Set ReaderId)
|
||||
, writerQueue :: AVar (Array (Effect Unit))
|
||||
, state :: AVar a
|
||||
}
|
||||
|
||||
makeReadGuard :: forall a. AVar ReaderId -> AVar (Set ReaderId) -> AVar a -> a -> Aff (ReadGuard a)
|
||||
makeReadGuard nextReaderIdCell readersCell stateCell state = do
|
||||
nextReaderId <- AVar.take nextReaderIdCell
|
||||
ids <- AVar.take readersCell
|
||||
AVar.put (nextReaderId + ReaderId 1) nextReaderIdCell
|
||||
AVar.put (Set.insert nextReaderId ids) readersCell
|
||||
AVar.put state stateCell
|
||||
pure $ ReadGuard nextReaderId state
|
||||
|
||||
mayWrite :: AVar (Array (Effect Unit)) -> Aff Unit
|
||||
mayWrite writerQCell = do
|
||||
writerQueue <- AVar.take writerQCell
|
||||
liftEffect $ for_ (Array.head writerQueue) identity
|
||||
AVar.put (fromMaybe [] $ Array.tail writerQueue) writerQCell
|
||||
|
||||
instance AsyncState RwLock WriteGuard ReadGuard where
|
||||
boxed a = liftAff do
|
||||
state <- AVar.new a
|
||||
nextReaderId <- AVar.new $ wrap 1
|
||||
readers <- AVar.new Set.empty
|
||||
writerQueue <- AVar.new mempty
|
||||
pure $ RwLock { state, nextReaderId, readers, writerQueue }
|
||||
|
||||
instance AsyncStateWritable RwLock WriteGuard where
|
||||
write _ (WriteGuard stateCell) s =
|
||||
liftAff
|
||||
$ const (AVar.put s stateCell)
|
||||
=<< liftMaybe (error "WriteGuard used after `unlock` invoked!")
|
||||
=<< AVar.tryTake stateCell
|
||||
|
||||
instance AsyncStateReadable RwLock WriteGuard where
|
||||
read _ (WriteGuard stateCell) =
|
||||
liftAff
|
||||
$ liftMaybe (error "WriteGuard used after `unlock` invoked!")
|
||||
=<< AVar.tryRead stateCell
|
||||
|
||||
instance AsyncStateReadable RwLock ReadGuard where
|
||||
read (RwLock { readers: readersCell }) (ReadGuard id a) = liftAff do
|
||||
readers <- AVar.read readersCell
|
||||
when (not $ Set.member id readers) $ throwError $ error "ReadGuard used after `unlock` invoked!"
|
||||
pure a
|
||||
|
||||
instance AsyncStateLock RwLock ReadGuard where
|
||||
unlock (RwLock { readers: readersCell, writerQueue: writerQCell }) (ReadGuard id _) = liftAff do
|
||||
readers <- Set.delete id <$> AVar.take readersCell
|
||||
AVar.put readers readersCell
|
||||
when (readers == Set.empty) $ mayWrite writerQCell
|
||||
tryLock (RwLock { readers: readersCell, state: stateCell, nextReaderId: nextReaderIdCell }) = liftAff do
|
||||
stateM <- AVar.tryTake stateCell
|
||||
for stateM $ makeReadGuard nextReaderIdCell readersCell stateCell
|
||||
lock (RwLock { readers: readersCell, state: stateCell, nextReaderId: nextReaderIdCell }) = liftAff do
|
||||
state <- AVar.take stateCell
|
||||
makeReadGuard nextReaderIdCell readersCell stateCell state
|
||||
|
||||
instance AsyncStateLock RwLock WriteGuard where
|
||||
unlock (RwLock { writerQueue: writerQCell, state: rwLockStateCell }) (WriteGuard localStateCell) = liftAff do
|
||||
state <- AVar.take localStateCell
|
||||
AVar.put state rwLockStateCell
|
||||
mayWrite writerQCell
|
||||
tryLock (RwLock { readers: readersCell, state: stateCell }) = liftAff do
|
||||
readers <- AVar.take readersCell
|
||||
if readers == Set.empty then do
|
||||
stateM <- AVar.tryTake stateCell
|
||||
AVar.put readers readersCell
|
||||
for stateM (map WriteGuard <<< AVar.new)
|
||||
else do
|
||||
AVar.put readers readersCell
|
||||
pure Nothing
|
||||
lock rw@(RwLock { writerQueue: writerQCell, readers: readersCell, state: stateCell }) = liftAff do
|
||||
let
|
||||
blockOnWritable_ cb = do
|
||||
launchAff_ do
|
||||
writerQ <- AVar.take writerQCell
|
||||
AVar.put (writerQ <> [ cb $ Right unit ]) writerQCell
|
||||
pure unit
|
||||
pure mempty
|
||||
blockOnWritable = makeAff blockOnWritable_
|
||||
onWritable = do
|
||||
state <- AVar.take stateCell
|
||||
WriteGuard <$> AVar.new state
|
||||
readers <- AVar.take readersCell
|
||||
if readers == Set.empty then do
|
||||
AVar.put readers readersCell
|
||||
onWritable
|
||||
else do
|
||||
AVar.put readers readersCell
|
||||
blockOnWritable
|
||||
lock rw
|
60
test/Test.Main.purs
Normal file
60
test/Test.Main.purs
Normal file
@ -0,0 +1,60 @@
|
||||
module Test.Main where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Control.Monad.Error.Class (try)
|
||||
import Control.Monad.State.Async (AsyncStateT, asyncModify, asyncPut, asyncRead, runAsyncState)
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Control.Parallel (parallel, sequential)
|
||||
import Data.Async.Class (class AsyncState)
|
||||
import Data.Async.Mutex (Mutex)
|
||||
import Data.Async.RwLock (RwLock)
|
||||
import Data.Either (isLeft)
|
||||
import Data.Identity (Identity)
|
||||
import Data.Newtype (wrap)
|
||||
import Effect (Effect)
|
||||
import Effect.Aff (Aff, delay, launchAff_)
|
||||
import Test.Spec (SpecT, describe, it)
|
||||
import Test.Spec.Assertions (shouldEqual, shouldSatisfy)
|
||||
import Test.Spec.Reporter (consoleReporter)
|
||||
import Test.Spec.Runner (runSpec)
|
||||
|
||||
common :: forall @w wl rl. AsyncState w wl rl => SpecT Aff Unit Identity Unit
|
||||
common = do
|
||||
it "does not throw" $ runAsyncState unit (pure @(AsyncStateT w Unit Aff) unit)
|
||||
it "supports single-threaded state manipulation" do
|
||||
runAsyncState 0 do
|
||||
pure @(AsyncStateT w Int Aff) unit
|
||||
flip shouldSatisfy isLeft =<< try (asyncRead $ shouldEqual 1)
|
||||
asyncPut 1
|
||||
asyncRead $ shouldEqual 1
|
||||
asyncModify $ \n -> pure $ n + 2
|
||||
asyncRead $ shouldEqual 3
|
||||
it "supports concurrent state manipulation" do
|
||||
let
|
||||
t1 = parallel do
|
||||
lift $ delay $ wrap 50.0
|
||||
asyncModify
|
||||
( \s -> do
|
||||
delay $ wrap 100.0
|
||||
pure $ s <> "john"
|
||||
)
|
||||
pure unit
|
||||
t2 = parallel do
|
||||
asyncPut "hello, "
|
||||
lift $ delay $ wrap 60.0
|
||||
asyncModify (pure <<< (_ <> "!"))
|
||||
pure unit
|
||||
|
||||
runAsyncState "" do
|
||||
pure @(AsyncStateT w String Aff) unit
|
||||
sequential (pure (\_ _ -> unit) <*> t1 <*> t2)
|
||||
asyncRead (shouldEqual "hello, john!")
|
||||
|
||||
main :: Effect Unit
|
||||
main = launchAff_ $ runSpec [ consoleReporter ] do
|
||||
describe "AsyncStateT" do
|
||||
describe "MutexStateT" do
|
||||
common @Mutex
|
||||
describe "RwLockStateT" do
|
||||
common @RwLock
|
Loading…
Reference in New Issue
Block a user