// History:
// 12 5 12 - Thibaut Colar Creation
using concurrent
using fwt
using camembert
**
** AxonSyncActor
** Actor to intercat with Axon / Skyspark backend
**
const class AxonSyncActor : Actor
{
const File projectFolder
const File dataFile
const File logFile
new make(File folder) : super(ActorPool())
{
this.projectFolder = folder
dataFile = projectFolder + `_sync_items.obj`
logFile = projectFolder + `_sync.log`
}
** Sync from/to server
override Obj? receive(Obj? obj)
{
AxonActorData? data
try
{
data = (AxonActorData) obj
result := doReceive(data)
data.runCallback(result)
return result
}
catch(Err e)
{
log(e, data)
if(e is IOErr || e.typeof.pod?.name == "sys") // for skysaprk errors don't diconnect
{
log("Unexpected error, trying to reconnect.", data)
reconnect(data)
}
return e
}
return null
}
Obj? doReceive(AxonActorData data)
{
action := data.action
if(action == AxonActorAction.needsPassword)
{
return ! Actor.locals.containsKey("camAxon.conn")
}
if(action == AxonActorAction.evalLast)
{
return AxonEvalStack.read.items.peek
}
if(action == AxonActorAction.evalUp)
{
return AxonEvalStack.read.up
}
if(action == AxonActorAction.evalDown)
{
return AxonEvalStack.read.down
}
if(action == AxonActorAction.autoStatus)
{
return Actor.locals.containsKey("camAxon.auto")
}
if(action == AxonActorAction.autoOn)
{
Actor.locals["camAxon.auto"] = true
return null
}
if(action == AxonActorAction.autoOff)
{
Actor.locals.remove("camAxon.auto")
return null
}
// Actions that need a connection
connect(data.password, data)
conn := (AxonConn) Actor.locals["camAxon.conn"]
switch(action)
{
case(AxonActorAction.deleteFunc):
log("Deleting from server function : $data.deleteFunc ", data)
del := "commit(diff(read(func and name==$data.deleteFunc.toCode), null, {remove}))"
return Unsafe(conn.client.evalAll([del])[0])
case(AxonActorAction.eval):
log("Eval: $data.eval ...", data)
AxonEvalStack.read.append(data.eval)
result := Unsafe(conn.client.evalAll([data.eval.trim])[0])
return result
case(AxonActorAction.sync):
AxonSyncInfo? info
auto := Actor.locals.containsKey("camAxon.auto")
try
info = sync(conn, data)
catch(Err syncErr)
{
log(syncErr, data)
if(! auto) throw syncErr
}
if(auto)
sendLater(2sec, data) // autosync
return info
default:
throw Err("Unexpected action: $action !")
}
}
** Connects the client (if not already connected)
Void connect(Str password, AxonActorData data)
{
if(! Actor.locals.containsKey("camAxon.conn"))
{
Actor.locals.remove("camAxon.data")
c := AxonConn.load(projectFolder + AxonConn.fileName)
log("Connecting to $c ...", data)
c.password = password
c.connect
Actor.locals["camAxon.conn"] = c
log("Connected ! (Version: ${c.client?.version})", data)
}
}
** Reconnect ... useful in case we got an IoErr, such as if we got logged out
Void reconnect(AxonActorData? data)
{
if(data != null)
{
log("Trying to reconnect ...", data)
conn := (AxonConn?) Actor.locals["camAxon.conn"]
if(conn != null)
{
conn?.connect
log("Reconnected !", data)
}
}
}
** Runs project synchronization with the server
AxonSyncInfo sync(AxonConn conn, AxonActorData data)
{
File[] sentItems := [,]
File[] createdItems := [,]
File[] updatedItems := [,]
Str:AxonSyncItem items := [:]
if(Actor.locals.containsKey("camAxon.data"))
items = (Str:AxonSyncItem) Actor.locals["camAxon.data"]
else
{
// first sync since app was started, try to reuse last run data
if(dataFile.exists)
try
items = dataFile.readObj()
catch(Err e) {e.trace}
}
grid := conn.client.evalAll([Str<|readAll(func).keepCols(["id", "mod", "name", "src"])|>])[0]
conn.dir.list.each |f|
{
if(f.ext == "axon")
{
if( ! items.containsKey(f.basename))
{
items[f.basename] = AxonSyncItem {it.path = relPath(f); it.localTs = f.modified.ticks; it.remoteTs = f.modified.ticks}
}
}
}
// sync from server files that don't exist locally or have a newer timestamp
grid.each |r|
{
f := conn.dir + `${r->name}.axon`
// new or updated file
if( ! items.containsKey(r->name) || r->mod->ticks > items[r->name].remoteTs)
{
if(f.exists)
updatedItems.add(f)
else
createdItems.add(f)
log("Pulling from sever : $f", data)
f.out.print(r->src).close
items[r->name] = AxonSyncItem {it.path = relPath(f); it.localTs = f.modified.ticks; it.remoteTs = r->mod->ticks}
}
}
// Push new files
conn.dir.list.each |f|
{
if(f.ext == "axon")
{
r := grid.find |row| {row->name == f.basename}
if(r==null || f.modified.ticks > items[f.basename].localTs)
{
sentItems.add(f)
log("Sending to server : $f", data)
src := f.readAllStr
expr := r == null ?
"commit(diff(null, {name: $f.basename.toCode, src: $src.toCode, mod: $f.modified.ticks, func}, {add}))"
: "commit(diff(read(func and name==$f.basename.toCode), {src: $src.toCode}))"
grid2 := conn.client.evalAll([expr])[0]
meta := grid2.meta
// Really should never happen unless inernal error
if(meta.has("errTrace"))
log("Error grid: " + meta["errTrace"], data)
newMod := grid2.first->mod->ticks
items[f.basename] = AxonSyncItem {it.path = relPath(f); it.localTs = f.modified.ticks; it.remoteTs = newMod}
}
}
}
Actor.locals["camAxon.data"] = items
// if any changes write the sync data file so it can get picked up if app is restarted
if( ! updatedItems.isEmpty || ! createdItems.isEmpty || ! sentItems.isEmpty)
dataFile.writeObj(items)
return AxonSyncInfo
{
updatedFiles = updatedItems
createdFiles = createdItems
sentFiles = sentItems
}
}
AxonEvalStack evalStack()
{
if(! Actor.locals.containsKey("camAxon.evalStack"))
Actor.locals["camAxon.evalStack"] = AxonEvalStack()
return Actor.locals["camAxon.evalStack"]
}
** Log to a file in the project for debugging / tracing
** Obj would typically be an Err or string
Void log(Obj obj, AxonActorData data)
{
data.runCallback(obj)
text := (obj is Err) ?
((Err) obj).traceToStr
: obj.toStr
// if file is old start over
if(logFile.exists && DateTime.now - logFile.modified > 1hr)
logFile.delete
if(! logFile.exists)
logFile.create
out := logFile.out(true)
try
out.printLine("${DateTime.now.toLocale} - $text")
catch(Err e)
e.trace
finally
out.close
}
** File path relative to project
** using this so that if project is relocated AxonSycItem serialization stays valid
Str relPath(File f)
{
return f.normalize.uri.relTo(projectFolder.normalize.uri).toStr
}
}
***********************************************************************
** Actor data object
**************************************************************************
@Serializable
const class AxonActorData
{
const AxonActorAction action
const Str? password := null
const Str? eval := null
const Sys? sys := null
const Str? deleteFunc := null
const |Obj?|? callback := null
new make(|This| f)
{
f(this)
}
Void runCallback(Obj? results)
{
if(callback != null)
{
Desktop.callAsync |->|
{
callback(results)
}
}
}
}
** SyncActor Actions enum
enum class AxonActorAction
{
needsPassword, sync, eval, evalUp, evalDown, evalLast,
autoOn, autoOff, autoStatus, deleteFunc
}
**************************************************************************
** AxonSyncItem
**************************************************************************
** Data about a synced file
** Note: serilized to obj, be careful if changing
@Serializable
internal const class AxonSyncItem
{
** Path relative to project folder
const Str path
const Int? localTs
const Int? remoteTs
new make(|This| f) {f(this)}
// Return a new instance for the same file but with an updated ts (both local & remote)
AxonSyncItem withTs(Int ts)
{
return AxonSyncItem
{
it.path = this.path
it.localTs = ts
it.remoteTs = ts
}
}
// New instance with new remoteTs
AxonSyncItem withLocalTs(Int ts)
{
return AxonSyncItem
{
it.path = this.path
it.localTs = ts
it.remoteTs = this.remoteTs
}
}
}
**************************************************************************
** Eval stack
**************************************************************************
const class AxonEvalStack
{
const Str[] items
const Int index
new make(Str[] items := ["readAll(ahu)"], Int index := 0)
{
this.items = items
this.index = index
}
Str up()
{
i := index <= 0 ? 0 : index -1
Actor.locals["camAxon.evalStack"] = AxonEvalStack(items, i)
return items[i]
}
Str down()
{
i := index >= items.size - 2 ? items.size - 1 : index + 1
Actor.locals["camAxon.evalStack"] = AxonEvalStack(items, i)
return items[i]
}
Void append(Str eval)
{
if(eval != items.peek)
Actor.locals["camAxon.evalStack"] = AxonEvalStack(items.dup.add(eval), index + 1)
}
static AxonEvalStack read()
{
if(! Actor.locals.containsKey("camAxon.evalStack"))
Actor.locals["camAxon.evalStack"] = AxonEvalStack()
return Actor.locals["camAxon.evalStack"]
}
}
**************************************************************************
** AxonActors
**************************************************************************
** Keep the map of actors per project/space
** Can't be held in space since those are reloaded all the time
class AxonActors
{
// map of project / actor
Uri:AxonSyncActor actors := [:]
AxonSyncActor forProject(File dir)
{
uri := dir.normalize.uri
if( ! actors.containsKey(uri))
actors[uri] = AxonSyncActor(dir)
return actors[uri]
}
}