mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
chore(folio): delete snapshot extractor files
This commit is contained in:
1 parent
d512e11385
commit
dafaff7ca9
12 files changed
-419
No files matched your search
-19
@@ -1,19 +0,0 @@
|
||||
package app.folio.sanderling
|
||||
|
||||
import app.folio.data.Repository
|
||||
import app.folio.feature.account.AddAccountUiState
|
||||
import app.folio.platform.balanceOf
|
||||
import dev.sanderling.sdk.Sanderling
|
||||
|
||||
object AccountSnapshots {
|
||||
val accountCount by Sanderling.snapshot { Repository.accounts.value.size }
|
||||
val totalBalance by Sanderling.snapshot { balanceOf(Repository.transactions.value) }
|
||||
val accounts by Sanderling.snapshot {
|
||||
val txns = Repository.transactions.value
|
||||
Repository.accounts.value.map { a ->
|
||||
val rows = txns.filter { it.accountId == a.id }
|
||||
mapOf("id" to a.id, "name" to a.name, "balance" to balanceOf(rows), "txnCount" to rows.size)
|
||||
}
|
||||
}
|
||||
val addAccountError by Sanderling.snapshot { AddAccountUiState.addAccountError.value }
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
package app.folio.sanderling
|
||||
|
||||
import app.folio.data.Repository
|
||||
import app.folio.feature.auth.LoginUiState
|
||||
import dev.sanderling.sdk.Sanderling
|
||||
|
||||
object AuthSnapshots {
|
||||
val loggedIn by Sanderling.snapshot { Repository.session.value != null }
|
||||
val authStatus by Sanderling.snapshot { if (Repository.session.value != null) "logged-in" else "logged-out" }
|
||||
val loginError by Sanderling.snapshot { LoginUiState.loginError.value }
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
package app.folio.sanderling
|
||||
|
||||
import app.folio.FocusTracker
|
||||
import app.folio.data.Repository
|
||||
import app.folio.data.TxnType
|
||||
import app.folio.feature.ledger.AddTransactionUiState
|
||||
import app.folio.navigation.Navigator
|
||||
import app.folio.navigation.Route
|
||||
import app.folio.platform.balanceOf
|
||||
import app.folio.platform.signedAmount
|
||||
import dev.sanderling.sdk.Sanderling
|
||||
|
||||
object LedgerSnapshots {
|
||||
private val activeId
|
||||
get() = when (val r = Navigator.current.value) {
|
||||
is Route.Ledger -> r.accountId
|
||||
is Route.AddTransaction -> r.accountId
|
||||
else -> null
|
||||
}
|
||||
|
||||
val activeAccountId by Sanderling.snapshot { activeId }
|
||||
val ledgerRows by Sanderling.snapshot {
|
||||
val active = activeId ?: return@snapshot emptyList<Any>()
|
||||
Repository.transactions.value.filter { it.accountId == active }.map {
|
||||
mapOf(
|
||||
"id" to it.id,
|
||||
"accountId" to it.accountId,
|
||||
"type" to if (it.type == TxnType.credit) "credit" else "debit",
|
||||
"amount" to it.amount,
|
||||
"signed" to signedAmount(it),
|
||||
)
|
||||
}
|
||||
}
|
||||
val ledgerBalance by Sanderling.snapshot {
|
||||
val active = activeId ?: return@snapshot 0L
|
||||
balanceOf(Repository.transactions.value.filter { it.accountId == active })
|
||||
}
|
||||
val focusedInput by Sanderling.snapshot { FocusTracker.current.value }
|
||||
val txnFormType by Sanderling.snapshot { AddTransactionUiState.txnFormType.value }
|
||||
val txnFormAccountId by Sanderling.snapshot { (Navigator.current.value as? Route.AddTransaction)?.accountId }
|
||||
val txnError by Sanderling.snapshot { AddTransactionUiState.txnError.value }
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
package app.folio.sanderling
|
||||
|
||||
import app.folio.navigation.Navigator
|
||||
import app.folio.navigation.Route
|
||||
import dev.sanderling.sdk.Sanderling
|
||||
|
||||
object NavigationSnapshots {
|
||||
val screen by Sanderling.snapshot {
|
||||
when (Navigator.current.value) {
|
||||
Route.Login -> "login"
|
||||
Route.Home -> "home"
|
||||
Route.AddAccount -> "add-account"
|
||||
is Route.Ledger -> "ledger"
|
||||
is Route.AddTransaction -> "add-transaction"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package app.folio.sanderling
|
||||
|
||||
import app.folio.data.Repository
|
||||
import app.folio.feature.account.AddAccountUiState
|
||||
import app.folio.platform.balanceOf
|
||||
|
||||
object AccountSnapshots {
|
||||
val accountCount by SanderlingIos.snapshot { Repository.accounts.value.size }
|
||||
val totalBalance by SanderlingIos.snapshot { balanceOf(Repository.transactions.value) }
|
||||
val accounts by SanderlingIos.snapshot {
|
||||
val txns = Repository.transactions.value
|
||||
Repository.accounts.value.map { a ->
|
||||
val rows = txns.filter { it.accountId == a.id }
|
||||
mapOf("id" to a.id, "name" to a.name, "balance" to balanceOf(rows), "txnCount" to rows.size)
|
||||
}
|
||||
}
|
||||
val addAccountError by SanderlingIos.snapshot { AddAccountUiState.addAccountError.value }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package app.folio.sanderling
|
||||
|
||||
import app.folio.data.Repository
|
||||
import app.folio.feature.auth.LoginUiState
|
||||
|
||||
object AuthSnapshots {
|
||||
val loggedIn by SanderlingIos.snapshot { Repository.session.value != null }
|
||||
val authStatus by SanderlingIos.snapshot { if (Repository.session.value != null) "logged-in" else "logged-out" }
|
||||
val loginError by SanderlingIos.snapshot { LoginUiState.loginError.value }
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package app.folio.sanderling
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import platform.Foundation.NSBundle
|
||||
import platform.Foundation.NSThread
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
internal object IosAgent {
|
||||
private const val VERSION = "0.0.1"
|
||||
private const val PROTOCOL_VERSION = 1
|
||||
|
||||
fun start(host: String, port: Int) {
|
||||
val thread = NSThread { runLoop(host, port) }
|
||||
thread.name = "sanderling-agent"
|
||||
thread.start()
|
||||
}
|
||||
|
||||
private fun runLoop(host: String, port: Int) {
|
||||
var delayMs = 0L
|
||||
while (true) {
|
||||
if (delayMs > 0) NSThread.sleepForTimeInterval(delayMs / 1000.0)
|
||||
try {
|
||||
val conn = TcpConnection.connect(host, port)
|
||||
delayMs = 0L
|
||||
try { serve(conn) } finally { conn.close() }
|
||||
} catch (_: Exception) {
|
||||
delayMs = if (delayMs <= 0L) 500L else minOf(delayMs * 2, 10_000L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun serve(conn: TcpConnection) {
|
||||
val appPackage = (NSBundle.mainBundle.infoDictionary?.get("CFBundleIdentifier") as? String) ?: "unknown"
|
||||
conn.writeFrame(
|
||||
"""{"type":"HELLO","protocol_version":$PROTOCOL_VERSION,"version":${jsonString(VERSION)},"platform":"ios","app_package":${jsonString(appPackage)}}""".encodeToByteArray()
|
||||
)
|
||||
while (true) {
|
||||
val frame = conn.readFrame()
|
||||
val text = frame.decodeToString()
|
||||
val type = extractJsonField(text, "type") ?: break
|
||||
val id = extractJsonLong(text, "id") ?: 0L
|
||||
when (type) {
|
||||
"PAUSE" -> handlePause(conn, id)
|
||||
"RESUME" -> IosPauser.release()
|
||||
"GOODBYE" -> return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePause(conn: TcpConnection, id: Long) {
|
||||
val snapshots = IosPauser.pauseAndSnapshot {
|
||||
val snap = SanderlingIos.extractors.toMap()
|
||||
buildMap { for ((name, extractor) in snap) put(name, runCatching { extractor() }.getOrNull()) }
|
||||
}
|
||||
val snapshotsJson = snapshots.entries.joinToString(",") { (k, v) -> "${jsonString(k)}:${jsonValue(v)}" }
|
||||
conn.writeFrame("""{"type":"STATE","id":$id,"snapshots":{$snapshotsJson}}""".encodeToByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
internal fun extractJsonField(json: String, key: String): String? =
|
||||
Regex("\"${Regex.escape(key)}\"\\s*:\\s*\"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"").find(json)?.groupValues?.get(1)
|
||||
|
||||
internal fun extractJsonLong(json: String, key: String): Long? =
|
||||
Regex("\"${Regex.escape(key)}\"\\s*:\\s*(-?\\d+)").find(json)?.groupValues?.get(1)?.toLongOrNull()
|
||||
|
||||
internal fun jsonString(s: String): String = buildString {
|
||||
append('"')
|
||||
for (c in s) when (c) {
|
||||
'"' -> append("\\\"")
|
||||
'\\' -> append("\\\\")
|
||||
'\n' -> append("\\n")
|
||||
'\r' -> append("\\r")
|
||||
'\t' -> append("\\t")
|
||||
else -> if (c.code < 0x20) append("\\u${c.code.toString(16).padStart(4, '0')}") else append(c)
|
||||
}
|
||||
append('"')
|
||||
}
|
||||
|
||||
internal fun jsonValue(value: Any?): String = when (value) {
|
||||
null -> "null"
|
||||
is Boolean -> if (value) "true" else "false"
|
||||
is Int -> value.toString()
|
||||
is Long -> value.toString()
|
||||
is Float -> value.toString()
|
||||
is Double -> value.toString()
|
||||
is String -> jsonString(value)
|
||||
is Map<*, *> -> "{${value.entries.joinToString(",") { (k, v) -> "${jsonString(k.toString())}:${jsonValue(v)}" }}}"
|
||||
is List<*> -> "[${value.joinToString(",") { jsonValue(it) }}]"
|
||||
else -> jsonString(value.toString())
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package app.folio.sanderling
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import platform.darwin.*
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
internal object IosPauser {
|
||||
private val snapshotReady = dispatch_semaphore_create(0)
|
||||
private val resumeGate = dispatch_semaphore_create(0)
|
||||
private var capturedSnapshot: Map<String, Any?> = emptyMap()
|
||||
|
||||
fun pauseAndSnapshot(extractors: () -> Map<String, Any?>): Map<String, Any?> {
|
||||
dispatch_async(dispatch_get_main_queue()) {
|
||||
capturedSnapshot = runCatching { extractors() }.getOrElse { emptyMap() }
|
||||
dispatch_semaphore_signal(snapshotReady)
|
||||
dispatch_semaphore_wait(resumeGate, dispatch_time(DISPATCH_TIME_NOW, 5_000_000_000L))
|
||||
}
|
||||
val waited = dispatch_semaphore_wait(snapshotReady, dispatch_time(DISPATCH_TIME_NOW, 5_000_000_000L))
|
||||
check(waited == 0L) { "snapshot timed out: main thread did not signal within 5s" }
|
||||
return capturedSnapshot
|
||||
}
|
||||
|
||||
fun release() {
|
||||
dispatch_semaphore_signal(resumeGate)
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package app.folio.sanderling
|
||||
|
||||
import app.folio.FocusTracker
|
||||
import app.folio.data.Repository
|
||||
import app.folio.data.TxnType
|
||||
import app.folio.feature.ledger.AddTransactionUiState
|
||||
import app.folio.navigation.Navigator
|
||||
import app.folio.navigation.Route
|
||||
import app.folio.platform.balanceOf
|
||||
import app.folio.platform.signedAmount
|
||||
|
||||
object LedgerSnapshots {
|
||||
private val activeId
|
||||
get() = when (val r = Navigator.current.value) {
|
||||
is Route.Ledger -> r.accountId
|
||||
is Route.AddTransaction -> r.accountId
|
||||
else -> null
|
||||
}
|
||||
|
||||
val activeAccountId by SanderlingIos.snapshot { activeId }
|
||||
val ledgerRows by SanderlingIos.snapshot {
|
||||
val active = activeId ?: return@snapshot emptyList<Any>()
|
||||
Repository.transactions.value.filter { it.accountId == active }.map {
|
||||
mapOf(
|
||||
"id" to it.id,
|
||||
"accountId" to it.accountId,
|
||||
"type" to if (it.type == TxnType.credit) "credit" else "debit",
|
||||
"amount" to it.amount,
|
||||
"signed" to signedAmount(it),
|
||||
)
|
||||
}
|
||||
}
|
||||
val ledgerBalance by SanderlingIos.snapshot {
|
||||
val active = activeId ?: return@snapshot 0L
|
||||
balanceOf(Repository.transactions.value.filter { it.accountId == active })
|
||||
}
|
||||
val focusedInput by SanderlingIos.snapshot { FocusTracker.current.value }
|
||||
val txnFormType by SanderlingIos.snapshot { AddTransactionUiState.txnFormType.value }
|
||||
val txnFormAccountId by SanderlingIos.snapshot { (Navigator.current.value as? Route.AddTransaction)?.accountId }
|
||||
val txnError by SanderlingIos.snapshot { AddTransactionUiState.txnError.value }
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
package app.folio.sanderling
|
||||
|
||||
import app.folio.navigation.Navigator
|
||||
import app.folio.navigation.Route
|
||||
|
||||
object NavigationSnapshots {
|
||||
val screen by SanderlingIos.snapshot {
|
||||
when (Navigator.current.value) {
|
||||
Route.Login -> "login"
|
||||
Route.Home -> "home"
|
||||
Route.AddAccount -> "add-account"
|
||||
is Route.Ledger -> "ledger"
|
||||
is Route.AddTransaction -> "add-transaction"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package app.folio.sanderling
|
||||
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
import platform.Foundation.NSProcessInfo
|
||||
|
||||
private fun String.camelToSnakeCase(): String = buildString {
|
||||
for ((i, c) in this@camelToSnakeCase.withIndex()) {
|
||||
if (c.isUpperCase() && i > 0) append('_')
|
||||
append(c.lowercaseChar())
|
||||
}
|
||||
}
|
||||
|
||||
object SanderlingIos {
|
||||
internal val extractors = mutableMapOf<String, () -> Any?>()
|
||||
|
||||
fun start() {
|
||||
val port = resolvePort() ?: return
|
||||
IosAgent.start("127.0.0.1", port)
|
||||
}
|
||||
|
||||
private fun resolvePort(): Int? {
|
||||
// Env var set via SIMCTL_CHILD_SANDERLING_PORT (simctl direct launch).
|
||||
(NSProcessInfo.processInfo.environment["SANDERLING_PORT"] as? String)
|
||||
?.toIntOrNull()?.let { return it }
|
||||
// Launch argument -SANDERLING_PORT <value> (Maestro simctl launch).
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val args = NSProcessInfo.processInfo.arguments as? List<String> ?: return null
|
||||
val idx = args.indexOfFirst { it == "-SANDERLING_PORT" }
|
||||
if (idx >= 0 && idx + 1 < args.size) {
|
||||
return args[idx + 1].toIntOrNull()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun extract(name: String, block: () -> Any?) {
|
||||
extractors[name] = block
|
||||
}
|
||||
|
||||
fun <T> snapshot(block: () -> T): SnapshotDelegate<T> = SnapshotDelegate(block)
|
||||
}
|
||||
|
||||
class SnapshotDelegate<T>(private val block: () -> T) {
|
||||
operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ReadOnlyProperty<Any?, T> {
|
||||
SanderlingIos.extract(prop.name.camelToSnakeCase(), block as () -> Any?)
|
||||
return ReadOnlyProperty { _, _ -> block() }
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package app.folio.sanderling
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import platform.posix.*
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
internal class TcpConnection private constructor(private val fd: Int) {
|
||||
companion object {
|
||||
fun connect(host: String, port: Int): TcpConnection {
|
||||
val sock = socket(AF_INET, SOCK_STREAM, 0)
|
||||
check(sock >= 0) { "socket() failed" }
|
||||
memScoped {
|
||||
val addr = alloc<sockaddr_in>()
|
||||
addr.sin_family = AF_INET.convert()
|
||||
addr.sin_port = networkShort(port)
|
||||
addr.sin_addr.s_addr = networkAddress(host)
|
||||
val result = platform.posix.connect(sock, addr.ptr.reinterpret(), sizeOf<sockaddr_in>().convert())
|
||||
if (result < 0) {
|
||||
close(sock)
|
||||
error("connect() to $host:$port failed: errno=$errno")
|
||||
}
|
||||
}
|
||||
return TcpConnection(sock)
|
||||
}
|
||||
|
||||
// Convert port to network byte order (big-endian bytes in memory on little-endian iOS).
|
||||
private fun networkShort(value: Int): UShort =
|
||||
(((value ushr 8) and 0xFF) or ((value and 0xFF) shl 8)).toUShort()
|
||||
|
||||
// Parse "a.b.c.d" → UInt with bytes [a,b,c,d] in memory (little-endian storage).
|
||||
private fun networkAddress(host: String): UInt {
|
||||
val parts = host.split(".").map { it.toInt() }
|
||||
check(parts.size == 4) { "expected IPv4 address, got: $host" }
|
||||
return (parts[0] or (parts[1] shl 8) or (parts[2] shl 16) or (parts[3] shl 24)).toUInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun writeFrame(data: ByteArray) {
|
||||
val len = data.size
|
||||
writeAll(byteArrayOf((len ushr 24).toByte(), (len ushr 16).toByte(), (len ushr 8).toByte(), len.toByte()))
|
||||
writeAll(data)
|
||||
}
|
||||
|
||||
fun readFrame(): ByteArray {
|
||||
val header = readAll(4)
|
||||
val len = ((header[0].toInt() and 0xFF) shl 24) or
|
||||
((header[1].toInt() and 0xFF) shl 16) or
|
||||
((header[2].toInt() and 0xFF) shl 8) or
|
||||
(header[3].toInt() and 0xFF)
|
||||
check(len in 0..16_777_216) { "bad frame length: $len" }
|
||||
return readAll(len)
|
||||
}
|
||||
|
||||
private fun writeAll(data: ByteArray) {
|
||||
data.usePinned { pinned ->
|
||||
var offset = 0
|
||||
while (offset < data.size) {
|
||||
val n = send(fd, pinned.addressOf(offset), (data.size - offset).convert(), 0).toInt()
|
||||
check(n > 0) { "send() failed: errno=$errno" }
|
||||
offset += n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readAll(count: Int): ByteArray {
|
||||
val buf = ByteArray(count)
|
||||
buf.usePinned { pinned ->
|
||||
var offset = 0
|
||||
while (offset < count) {
|
||||
val n = recv(fd, pinned.addressOf(offset), (count - offset).convert(), 0).toInt()
|
||||
check(n > 0) { "recv() returned $n" }
|
||||
offset += n
|
||||
}
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
fun close() {
|
||||
platform.posix.close(fd)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user