Skip to content

Commit

Permalink
Set up Replicator as Elm-Land "Component"
Browse files Browse the repository at this point in the history
  • Loading branch information
Erudition committed Nov 12, 2023
1 parent c3513a3 commit ef0bc3b
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 28 deletions.
182 changes: 182 additions & 0 deletions elm/Components/Replicator.elm
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
module Components.Replicator exposing (..)

import Console
import Log
import Maybe.Extra
import Platform exposing (Task)
import Replicated.Change as Change
import Replicated.Codec as Codec exposing (SkelCodec)
import Replicated.Node.Node as Node exposing (Node, OpImportWarning)
import Replicated.Op.Op as Op
import SmartTime.Moment as Moment exposing (Moment)
import Task


{-| Internal Model of the replicator component.
-}
type Replicator replica
= ReplicatorModel
{ node : Node
, replicaCodec : SkelCodec ReplicaError replica
, replica : replica
, outPort : String -> Cmd Msg
}


type alias ReplicaError =
String


{-| Data required to initialize the replicator.
-}
type alias ReplicatorConfig replica =
{ launchTime : Maybe Moment
, replicaCodec : SkelCodec ReplicaError replica
, outPort : String -> Cmd Msg
}


init : ReplicatorConfig replica -> ( Replicator replica, replica )
init { launchTime, replicaCodec, outPort } =
let
( startNode, initChanges ) =
Codec.startNodeFromRoot launchTime replicaCodec

( startReplica, replicaDecodeWarnings ) =
Codec.forceDecodeFromNode replicaCodec startNode
in
-- TODO return warnings?
( ReplicatorModel
{ node = startNode
, replicaCodec = replicaCodec
, replica = startReplica
, outPort = outPort
}
, startReplica
)


{-| This component's internal Msg type.
-}
type Msg
= LoadRon Int (List String)
| ApplyFrames (List Change.Frame) Moment


update :
Msg
-> Replicator replica
-> { newReplicator : Replicator replica, newReplica : replica, warnings : List OpImportWarning, cmd : Cmd Msg }
update msg (ReplicatorModel oldReplicator) =
case msg of
LoadRon originalFrameCount [] ->
let
( newReplica, problemMaybe ) =
Codec.forceDecodeFromNode oldReplicator.replicaCodec oldReplicator.node

problemAsWarning =
case problemMaybe of
Just codecErr ->
-- TODO convert to warning
Log.crashInDev (Codec.errorToString codecErr) []

Nothing ->
[]

newReplicator =
ReplicatorModel
{ oldReplicator | replica = newReplica }
in
{ newReplicator = newReplicator
, newReplica = newReplica
, warnings = problemAsWarning
, cmd = Cmd.none
}

LoadRon originalFrameCount (nextRonFrame :: moreRonFrames) ->
let
{ node, warnings, newObjects } =
Node.updateWithRon { node = oldReplicator.node, warnings = [], newObjects = [] } (Log.logMessageOnly ("Importing RON frame: \n" ++ nextRonFrame) nextRonFrame)

progress =
originalFrameCount - List.length moreRonFrames

newReplicator =
ReplicatorModel
{ oldReplicator | node = node }
in
{ newReplicator = newReplicator
, newReplica = oldReplicator.replica
, warnings = warnings
, cmd = Task.perform (\_ -> LoadRon originalFrameCount moreRonFrames) (Task.succeed ())
}

ApplyFrames newFrames newTime ->
let
( nodeWithUpdates, finalOutputFrame ) =
List.foldl applyFrame ( oldReplicator.node, [] ) newFrames

applyFrame givenFrame ( inNode, outputsSoFar ) =
let
{ outputFrame, updatedNode } =
Node.apply (Just newTime) False inNode givenFrame
in
( updatedNode, outputsSoFar ++ outputFrame )
in
case Codec.decodeFromNode oldReplicator.replicaCodec nodeWithUpdates of
Ok updatedUserReplica ->
{ newReplicator = ReplicatorModel { oldReplicator | node = nodeWithUpdates, replica = updatedUserReplica }
, newReplica = updatedUserReplica
, warnings = []
, cmd = Cmd.batch [ oldReplicator.outPort (Op.closedChunksToFrameText finalOutputFrame) ]
}

Err problem ->
{ newReplicator =
Log.logSeparate (Console.bgRed "Failed to decodeFromNode! Reverting update! Ops:\n" ++ Console.colorsInverted (Op.closedChunksToFrameText finalOutputFrame) ++ "\nProblem: ")
problem
(ReplicatorModel oldReplicator)
, newReplica = oldReplicator.replica
, warnings = [] -- TODO warn if fail to apply
, cmd = Cmd.none
}


{-| Type for your "incoming frames" port. Use this on your JS port which is called when you receive new changeframes from elsewhere. The RON data (as a string) will be processed into the replicator.
-}
type alias IncomingFramesPort =
(String -> Msg) -> Sub Msg


{-| Wire this component's subscriptions up into your `Shared.subscriptions`, using `Sub.map` to convert it to your message type, like:
subscriptions =
Sub.batch
[ ...
, Sub.map ReplicatorUpdate (Components.Replicator.subscriptions incomingRon)
]
`incomingRon` is a port you create (you can put it in the `Effect` module if you like) that receives a String, and has the type `IncomingFramesPort`.
-}
subscriptions : IncomingFramesPort -> Sub Msg
subscriptions incomingFramesPort =
let
splitIncomingFrames inRon =
let
frames =
String.split "" inRon
in
LoadRon (List.length frames) frames
in
incomingFramesPort splitIncomingFrames


saveEffect : List Change.Frame -> Cmd Msg
saveEffect framesToSave =
case Change.nonEmptyFrames framesToSave of
[] ->
Cmd.none

_ ->
Task.perform (ApplyFrames framesToSave) Moment.now
24 changes: 17 additions & 7 deletions elm/Effect.elm
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ port module Effect exposing
, sendCmd, sendMsg
, pushRoute, replaceRoute, loadExternalUrl
, map, toCmd
, PromptOptions, clearPreferences, closePopup, dialogPrompt, mlPredict, requestNotificationPermission, saveChanges, saveFrame, sendNotifications, syncMarvin, syncTodoist, toast
, PromptOptions, clearPreferences, closePopup, dialogPrompt, incomingRon, mlPredict, requestNotificationPermission, saveChanges, saveFrame, saveFrames, sendNotifications, setStorage, syncMarvin, syncTodoist, toast
)

{-|
Expand All @@ -19,6 +19,7 @@ port module Effect exposing
-}

import Browser.Navigation
import Components.Replicator
import Dict exposing (Dict)
import Http
import Integrations.Marvin as Marvin
Expand Down Expand Up @@ -58,7 +59,7 @@ type Effect msg
-- SHARED
| SendSharedMsg Shared.Msg.Msg
-- REPLICATOR
| StoreFrame Change.Frame
| Save (List Change.Frame)
-- EXTERNAL APP
| ClearPreferences
| RequestNotificationPermission
Expand Down Expand Up @@ -266,7 +267,12 @@ port toastPort : String -> Cmd msg

saveFrame : Change.Frame -> Effect msg
saveFrame changeFrame =
StoreFrame changeFrame
Save [ changeFrame ]


saveFrames : List Change.Frame -> Effect msg
saveFrames changeFrames =
Save changeFrames


saveChanges : String -> List Change.Change -> Effect msg
Expand Down Expand Up @@ -358,6 +364,9 @@ requestNotificationPermission =
RequestNotificationPermission


port incomingRon : (String -> msg) -> Sub msg



-- INTERNALS

Expand Down Expand Up @@ -404,8 +413,8 @@ map fn effect =
SendNotifications list ->
SendNotifications list

StoreFrame frame ->
StoreFrame frame
Save frame ->
Save frame

ClosePopup ->
ClosePopup
Expand Down Expand Up @@ -481,8 +490,9 @@ toCmd options effect =
SendNotifications notifList ->
notificationsToJS notifList

StoreFrame frame ->
Debug.todo "setStorage port"
Save frames ->
Cmd.map (options.fromSharedMsg << Shared.Msg.ReplicatorUpdate) <|
Components.Replicator.saveEffect frames

ClosePopup ->
Debug.todo "close popup port/taskport"
Expand Down
3 changes: 1 addition & 2 deletions elm/Replicated/Codec.elm
Original file line number Diff line number Diff line change
Expand Up @@ -730,8 +730,7 @@ replaceForUrl =
Regex.fromString "[\\+/=]" |> Maybe.withDefault Regex.never


{-| Generates naked Changes from a Codec's default values. These are all the values that would normally be skipped, not encoded to Changes.
Useful for spitting out test data, and seeing the whole heirarchy of your types.
{-| Start a new node
-}
startNodeFromRoot : Maybe Moment -> WrappedOrSkelCodec e s a -> ( Node, List Op.ClosedChunk )
startNodeFromRoot maybeMoment rootCodec =
Expand Down
6 changes: 2 additions & 4 deletions elm/Replicated/Node/Node.elm
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import Replicated.Identifier exposing (..)
import Replicated.Node.NodeID as NodeID exposing (NodeID)
import Replicated.Object as Object exposing (Object)
import Replicated.Op.Op as Op exposing (Op, ReducerID, create)
import Replicated.Op.OpID as OpID exposing (InCounter, ObjectID, ObjectIDString, OpID, OutCounter)
import Replicated.Op.OpID as OpID exposing (InCounter, ObjectID, ObjectIDString, OpID, OpIDSortable, OutCounter)
import Set exposing (Set)
import SmartTime.Moment exposing (Moment)

Expand All @@ -25,16 +25,14 @@ import SmartTime.Moment exposing (Moment)
type alias Node =
{ identity : NodeID
, ops : OpDb

-- , objects : Dict OpID.OpIDSortable
, root : Maybe ObjectID
, highestSeenClock : Int
, peers : List Peer
}


type alias OpDb =
AnyDict OpID.OpIDSortable OpID Op
AnyDict OpIDSortable OpID Op


type alias InitArgs =
Expand Down
Loading

0 comments on commit ef0bc3b

Please sign in to comment.