{"id":39276270,"url":"https://github.com/slne-development/surf-redis","last_synced_at":"2026-04-05T21:14:10.469Z","repository":{"id":330153155,"uuid":"1120533411","full_name":"SLNE-Development/surf-redis","owner":"SLNE-Development","description":"Eine Kotlin-Bibliothek für Redis-basierte, verteilte Systeme mit Lettuce und Kotlin Coroutines. Bietet eine asynchrone, typsichere Lösung für Event-Verteilung, Request-Response-Kommunikation und synchronisierte Datenstrukturen über mehrere Server oder Instanzen hinweg.","archived":false,"fork":false,"pushed_at":"2026-01-17T21:53:26.000Z","size":1126,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-01-18T11:39:19.887Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SLNE-Development.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"SLNE-Development"}},"created_at":"2025-12-21T12:27:46.000Z","updated_at":"2026-01-17T21:53:29.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/SLNE-Development/surf-redis","commit_stats":null,"previous_names":["slne-development/surf-redis"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/SLNE-Development/surf-redis","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SLNE-Development%2Fsurf-redis","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SLNE-Development%2Fsurf-redis/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SLNE-Development%2Fsurf-redis/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SLNE-Development%2Fsurf-redis/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SLNE-Development","download_url":"https://codeload.github.com/SLNE-Development/surf-redis/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SLNE-Development%2Fsurf-redis/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28755045,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-25T13:59:49.818Z","status":"ssl_error","status_checked_at":"2026-01-25T13:59:33.728Z","response_time":113,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-18T00:56:13.504Z","updated_at":"2026-04-05T21:14:10.454Z","avatar_url":"https://github.com/SLNE-Development.png","language":"Kotlin","readme":"# surf-redis\n\n`surf-redis` is a Redis-based coordination and synchronization library designed for distributed\nJVM applications.\n\nIt provides:\n- a central Redis API abstraction\n- event broadcasting\n- request/response messaging\n- replicated in-memory data structures (value, list, set, map)\n- simple Redis-backed caches\n\nThe library is built on top of **Redisson**, **Reactor**, and **Kotlin coroutines**.\n\n---\n\n## Concepts\n\n### RedisApi\n\n`RedisApi` is the central entry point.  \nIt owns the Redis clients and manages the lifecycle of all Redis-backed components.\n\nA typical application creates **exactly one** `RedisApi` instance and shares it across the system.\n\nLifecycle:\n1. Create the API\n2. Register listeners / handlers / sync structures\n3. Freeze the API\n4. Connect to Redis\n5. Disconnect on shutdown\n\n```kotlin\nval redisApi = RedisApi.create()\n\nredisApi.subscribeToEvents(SomeListener())\nredisApi.registerRequestHandler(SomeRequestHandler())\n\nredisApi.freezeAndConnect()\n````\n\n---\n\n## Service Pattern (recommended)\n\nIn most applications, `RedisApi` is wrapped inside a service that owns its lifecycle and exposes\na global access point.\n\n```kotlin\nabstract class RedisService {\n\n    val redisApi = RedisApi.create()\n\n    fun connect() {\n        register()\n        redisApi.freezeAndConnect()\n    }\n\n    @MustBeInvokedByOverriders\n    @ApiStatus.OverrideOnly\n    protected open fun register() {\n        // register listeners and request handlers\n    }\n\n    fun disconnect() {\n        redisApi.disconnect()\n    }\n\n    companion object {\n        val instance = requiredService\u003cRedisService\u003e()\n        fun get() = instance\n\n        fun publish(event: RedisEvent) =\n            instance.redisApi.publishEvent(event)\n\n        fun namespaced(suffix: String) =\n            \"example-namespace:$suffix\"\n    }\n}\n\nval redisApi get() = RedisService.get().redisApi\n\n@AutoService(RedisService::class)\nclass PaperRedisService : RedisService() {\n    override fun register() {\n        super.register()\n        // redisApi.subscribeToEvents(PaperListener())\n    }\n}\n```\n\nSync structures are typically created **where they are used**, not inside the Redis service:\n\n```kotlin\nobject SomeOtherService {\n    private val counter =\n        redisApi.createSyncValue\u003cInt\u003e(\n            RedisService.namespaced(\"counter\"),\n            defaultValue = 0\n        )\n}\n```\n\n---\n\n## Events\n\n### RedisEvent\n\nEvents are broadcasted via Redis Pub/Sub.\n\n```kotlin\n@Serializable\nclass PlayerJoinedEvent(val playerId: UUID) : RedisEvent()\n```\n\nPublishing an event:\n\n```kotlin\nRedisService.publish(PlayerJoinedEvent(playerId))\n```\n\n### Event handlers\n\nHandlers are discovered via `@OnRedisEvent`.\n\n```kotlin\nclass PlayerListener {\n\n    @OnRedisEvent\n    fun onJoin(event: PlayerJoinedEvent) {\n        if (event.originatesFromThisClient()) return\n        println(\"Player joined on another node: ${event.playerId}\")\n    }\n}\n```\n\nHandlers can also be `suspend` functions — they run directly in the listener coroutine:\n\n```kotlin\nclass PlayerListener {\n\n    @OnRedisEvent\n    suspend fun onJoin(event: PlayerJoinedEvent) {\n        if (event.originatesFromThisClient()) return\n        val profile = fetchProfileSuspending(event.playerId)\n        println(\"Profile: $profile\")\n    }\n}\n```\n\n* Handlers are invoked inside a coroutine on **`Dispatchers.Default`**\n* Both regular and `suspend` functions are supported\n* **Do not perform blocking work directly** — switch dispatchers for blocking I/O:\n\n```kotlin\n@OnRedisEvent\nsuspend fun onJoin(event: PlayerJoinedEvent) {\n    withContext(Dispatchers.IO) {\n        loadFromDatabase(event.playerId)\n    }\n}\n```\n\n---\n\n## Request / Response\n\nRequests are **broadcasted**.\nMultiple servers may respond.\nThe **first response that arrives wins**.\n\n### Defining requests and responses\n\n```kotlin\n@Serializable\nclass GetPlayerRequest(val playerId: UUID) : RedisRequest()\n\n@Serializable\nclass PlayerResponse(val name: String) : RedisResponse()\n```\n\n### Handling requests\n\n```kotlin\nclass PlayerRequestHandler {\n\n    @HandleRedisRequest\n    fun handle(ctx: RequestContext\u003cGetPlayerRequest\u003e) {\n        if (ctx.originatesFromThisClient()) return\n\n        val player = loadPlayer(ctx.request.playerId)\n        ctx.respond(PlayerResponse(player.name))\n    }\n}\n```\n\nHandlers can also be `suspend` functions — [RequestContext.respond] can be called at any suspension point:\n\n```kotlin\nclass PlayerRequestHandler {\n\n    @HandleRedisRequest\n    suspend fun handle(ctx: RequestContext\u003cGetPlayerRequest\u003e) {\n        if (ctx.originatesFromThisClient()) return\n\n        val player = fetchPlayerSuspending(ctx.request.playerId)\n        ctx.respond(PlayerResponse(player.name))\n    }\n}\n```\n\n* Handlers are invoked inside a coroutine on **`Dispatchers.Default`**\n* Both regular and `suspend` functions are supported\n* **Do not perform blocking work directly** — switch to `Dispatchers.IO` for blocking calls:\n\n```kotlin\n@HandleRedisRequest\nsuspend fun handle(ctx: RequestContext\u003cGetPlayerRequest\u003e) {\n    if (ctx.originatesFromThisClient()) return\n    val player = withContext(Dispatchers.IO) { loadFromDatabaseBlocking(ctx.request.playerId) }\n    ctx.respond(PlayerResponse(player.name))\n}\n```\n\nEach handler may respond **at most once**.\n\n### Sending requests\n\n```kotlin\nval response: PlayerResponse =\n    redisApi.sendRequest(GetPlayerRequest(playerId))\n```\n\nIf no response is received within the timeout, a `RequestTimeoutException` is thrown.\n\n---\n\n## Sync Structures\n\nSync structures maintain local in-memory state and synchronize it via Redis.\nThey are **eventually consistent**.\n\nAll sync structures:\n\n* are created via `RedisApi`\n* expose a local view\n* propagate mutations asynchronously\n* provide listener support\n\n### SyncValue\n\nReplicated single value.\n\n```kotlin\nval onlineCount =\n    redisApi.createSyncValue(\n        RedisService.namespaced(\"online-count\"),\n        defaultValue = 0\n    )\n\nonlineCount.set(onlineCount.get() + 1)\n```\n\nProperty delegate support:\n\n```kotlin\nvar count by onlineCount.asProperty()\ncount++\n```\n\n### SyncList\n\nReplicated ordered list.\n\n```kotlin\nval players =\n    redisApi.createSyncList\u003cString\u003e(\n        RedisService.namespaced(\"players\")\n    )\n\nplayers += \"Alice\"\nplayers.remove(\"Bob\")\n```\n\n### SyncSet\n\nReplicated set.\n\n```kotlin\nval onlinePlayers =\n    redisApi.createSyncSet\u003cString\u003e(\n        RedisService.namespaced(\"online-players\")\n    )\n\nonlinePlayers += \"Alice\"\nonlinePlayers -= \"Bob\"\n```\n\n### SyncMap\n\nReplicated key-value map.\n\n```kotlin\nval playerScores =\n    redisApi.createSyncMap\u003cUUID, Int\u003e(\n        RedisService.namespaced(\"scores\")\n    )\n\nplayerScores[playerId] = 42\n```\n\n---\n\n## Listeners\n\nAll sync structures support listeners:\n\n```kotlin\nplayerScores.addListener { change -\u003e\n    when (change) {\n        is SyncMapChange.Put -\u003e println(\"Score updated: ${change.key}\")\n        is SyncMapChange.Removed -\u003e println(\"Score removed: ${change.key}\")\n        is SyncMapChange.Cleared -\u003e println(\"Scores cleared\")\n    }\n}\n```\n\nListener invocation thread depends on whether the change is local or remote.\n\n---\n\n## Caches\n\n### SimpleRedisCache\n\nKey-value cache with TTL.\n\n```kotlin\nval cache =\n    redisApi.createSimpleCache\u003cString, Player\u003e(\n        namespace = \"players\",\n        ttl = 30.seconds\n    )\n\ncache.put(\"alice\", player)\nval cached = cache.get(\"alice\")\n```\n\n### SimpleSetRedisCache\n\nSet-based cache with optional indexes.\n\n```kotlin\nval cache =\n    redisApi.createSimpleSetRedisCache(\n        namespace = \"players\",\n        ttl = 30.seconds,\n        idOf = { it.id.toString() }\n    )\n```\n\n---\n\n## Internal APIs\n\nSome APIs are annotated with `@InternalRedisAPI`.\n\nThese APIs:\n\n* are **not stable**\n* may change or be removed without notice\n* must not be used by consumers\n\n---\n\n## Handler Dispatch (Internals)\n\nEvent handlers (`@OnRedisEvent`) and request handlers (`@HandleRedisRequest`) are dispatched\nthrough **JVM hidden classes** generated at registration time.\n\nEach handler method is resolved into a `MethodHandle`, which is then embedded as a\n`static final` constant in a hidden class implementing `RedisEventInvoker` or\n`RedisRequestHandlerInvoker`. This allows the JIT compiler to constant-fold and inline the\nentire dispatch path — significantly outperforming raw `MethodHandle.invoke()` polymorphic calls.\n\nHidden classes are:\n\n* **Not discoverable** by name — no classloader pollution\n* **Garbage-collectible** when no longer referenced\n* **JIT-friendly** — the dispatch target is a compile-time constant\n\nThis approach combines the flexibility of reflection-based handler discovery with\nnear-direct-call performance at runtime.\n\n\u003e [!NOTE]\n\u003e This is an implementation detail. The public API for registering handlers\n\u003e (`@OnRedisEvent`, `@HandleRedisRequest`) remains unchanged.\n\n---\n\n## Guarantees \u0026 Non-Guarantees\n\nGuaranteed:\n\n* Local operations are non-blocking\n* Eventual convergence across nodes\n* At-most-once response per request handler\n* First response wins for requests\n\nNot guaranteed:\n\n* Strong consistency\n* Total ordering across nodes\n* Delivery in the presence of network partitions\n* Exactly-once semantics\n\n---\n\n## Common Pitfalls\n\n### 1. Creating sync structures after `freeze()`\n\nAll sync structures (`SyncValue`, `SyncList`, `SyncSet`, `SyncMap`) **must be created before**\n`RedisApi.freeze()` is called.\n\nThis can be subtle in larger applications, because sync structures are often created in\n*other services* that depend on Redis, not inside the Redis service itself.\n\n**Problematic setup**\n\n```kotlin\nclass SomeService {\n    private val counter =\n        redisApi.createSyncValue(\"counter\", 0) // \u003c-- may run too late\n}\n````\n\nIf `SomeService` is initialized **after** `RedisApi.freeze()` has already been called,\nsync structure creation will fail or behave incorrectly.\n\n**Correct approach**\n\nYou must ensure that **all services that create sync structures are initialized\nbefore the Redis API is frozen**.\n\nA common pattern is:\n\n* RedisService owns the `RedisApi`\n* All dependent services are initialized *before* `connect()` is called\n\n```kotlin\nclass RedisService {\n\n    val redisApi = RedisApi.create()\n\n    fun connect() {\n        // Ensure all services that create sync structures are initialized here\n        SomeService.init()\n        AnotherService.init()\n\n        redisApi.freezeAndConnect()\n    }\n}\n```\n\nAlternatively, ensure that service initialization order guarantees that\nall sync structures are created eagerly during startup.\n\n**Rule of thumb:**\n\n\u003e [!TIP]\n\u003e If a service creates sync structures, it must be initialized before `RedisApi.freeze()`.\n\n---\n\n### 2. Treating sync structures as strongly consistent\n\nSync structures are **eventually consistent**.\n\nDo **not** assume:\n\n* immediate visibility on other nodes\n* global ordering guarantees\n* exactly-once delivery\n\nThey are designed for coordination and shared state, not transactional consistency.\n\n---\n\n### 3. Doing blocking work in event or request handlers\n\nEvent handlers (`@OnRedisEvent`) and request handlers (`@HandleRedisRequest`) are invoked\ninside a coroutine launched on **`Dispatchers.Default`**.\n\nBoth regular and `suspend` handler methods are supported. However, even though handlers run\nin a coroutine, **do not perform blocking work directly**:\n\n* blocking database drivers\n* file I/O\n* blocking network calls\n* long CPU-intensive computations\n\nBlocking a `Dispatchers.Default` thread stalls the shared thread pool and degrades the entire application.\nAlways switch the dispatcher for blocking operations.\n\n`RequestContext` implements `CoroutineScope` — you can `launch` additional coroutines from it.\nPick the right dispatcher for the work being done:\n\n```kotlin\n// CPU-intensive work — Default dispatcher is already correct, no switch needed\n@HandleRedisRequest\nsuspend fun handle(ctx: RequestContext\u003cMyRequest\u003e) {\n    val result = computeSomething()\n    ctx.respond(MyResponse(result))\n}\n\n// Blocking I/O (blocking DB drivers, file access, blocking network calls) — switch to Dispatchers.IO\n@HandleRedisRequest\nsuspend fun handle(ctx: RequestContext\u003cMyRequest\u003e) {\n    val result = withContext(Dispatchers.IO) {\n        loadFromDatabaseBlocking()\n    }\n    ctx.respond(MyResponse(result))\n}\n\n// Non-blocking / suspending I/O (e.g. R2DBC, async clients) — no dispatcher switch needed\n@HandleRedisRequest\nsuspend fun handle(ctx: RequestContext\u003cMyRequest\u003e) {\n    val result = loadFromDatabaseSuspending()\n    ctx.respond(MyResponse(result))\n}\n\n// From a non-suspend handler — launch a coroutine manually\n@HandleRedisRequest\nfun handle(ctx: RequestContext\u003cMyRequest\u003e) {\n    ctx.launch(Dispatchers.IO) {\n        val result = loadFromDatabaseBlocking()\n        ctx.respond(MyResponse(result))\n    }\n}\n```\n\nThe same applies to event handlers:\n\n```kotlin\n// Suspend event handler with blocking I/O\n@OnRedisEvent\nsuspend fun onJoin(event: PlayerJoinedEvent) {\n    if (event.originatesFromThisClient()) return\n    withContext(Dispatchers.IO) {\n        loadFromDatabase(event.playerId)\n    }\n}\n```\n\n---\n\n### 4. Responding multiple times to a request\n\nEach `RequestContext` allows **exactly one** response.\n\nCalling `respond()` more than once will throw an exception.\nThis includes responding both synchronously *and* asynchronously by mistake.\n\n---\n\n### 5. Ignoring `originatesFromThisClient()`\n\nRequests and events are broadcasted.\n\nIf a handler should not process self-originated messages, it must explicitly check:\n\n```kotlin\nif (event.originatesFromThisClient()) return\n```\n\nThis is especially important to avoid feedback loops.\n\n---\n\n### 6. Assuming sync structures survive full cluster shutdown\n\nSync structures rely on Redis keys with TTL.\n\nIf **all nodes go offline**, the remote state may expire.\nThe next node will start from the default/snapshot behavior defined by the structure.\n\nDesign your application logic accordingly.","funding_links":["https://github.com/sponsors/SLNE-Development"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fslne-development%2Fsurf-redis","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fslne-development%2Fsurf-redis","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fslne-development%2Fsurf-redis/lists"}