use-truapi
Statements

useStatementChannel

Last-write-wins channels for presence, cursors and ephemeral state

useStatementChannel layers last-write-wins channels on top of the statement store. Where useStatements accumulates a feed, a channel keeps exactly one value per channel name: a newer write replaces the previous one. Use it for presence, live cursors, typing indicators and other state where only the latest value matters.

Keys in values are hex-encoded channel hashes, not the names you wrote to, so put identifying fields (peer id, user name) in the payload and render from those. A timestamp field in the payload drives last-write-wins ordering; when omitted, the current time is filled in on write.

Channels are host-only. Standalone, ready stays false, values stays empty, and write resolves false.

Usage

A typed presence channel: each client heartbeats its own channel on an interval and renders everyone's latest announcement from the values map.

import { useStatementChannel } from "@use-truapi/react";
import { useEffect } from "react";

interface Presence {
  peerId: string;
  name: string;
  timestamp: number;
}

function WhoIsOnline({ peerId, name }: { peerId: string; name: string }) {
  const { values, write, ready } = useStatementChannel<Presence>();

  useEffect(() => {
    if (!ready) return;
    const announce = () =>
      write(`presence/${peerId}`, { peerId, name, timestamp: Date.now() });
    void announce();
    const interval = setInterval(announce, 10_000);
    return () => clearInterval(interval);
  }, [ready, write, peerId, name]);

  if (!ready) return <p>Presence unavailable (running standalone).</p>;

  return (
    <ul>
      {[...values.values()].map((peer) => (
        <li key={peer.peerId}>
          {peer.name}, seen {new Date(peer.timestamp).toLocaleTimeString()}
        </li>
      ))}
    </ul>
  );
}
<script setup lang="ts">
import { useStatementChannel } from "@use-truapi/vue";
import { onUnmounted, watch } from "vue";

interface Presence {
  peerId: string;
  name: string;
  timestamp: number;
}

const props = defineProps<{ peerId: string; name: string }>();

const { values, write, ready } = useStatementChannel<Presence>();

let interval: ReturnType<typeof setInterval> | undefined;
watch(ready, (isReady) => {
  if (!isReady) return;
  const announce = () =>
    write(`presence/${props.peerId}`, {
      peerId: props.peerId,
      name: props.name,
      timestamp: Date.now(),
    });
  void announce();
  interval = setInterval(announce, 10_000);
});
onUnmounted(() => clearInterval(interval));
</script>

<template>
  <p v-if="!ready">Presence unavailable (running standalone).</p>
  <ul v-else>
    <li v-for="peer in [...values.values()]" :key="peer.peerId">
      {{ peer.name }}, seen {{ new Date(peer.timestamp).toLocaleTimeString() }}
    </li>
  </ul>
</template>

In Vue, values and ready are refs: access them with .value in script and directly in templates.

Channels live under your app topic; pass topic2 to partition them further. Two components with different topic2 values see fully independent channel maps:

const cursors = useStatementChannel<Cursor>({ topic2: `doc-${docId}` });

See also

API reference

React

function useStatementChannel<T>(options?): StatementChannel<T>;

Last-write-wins channels (presence, live cursors, ephemeral app state). ready stays false standalone.

Type Parameters

Type Parameter
T extends object

Parameters

ParameterType
options?{ topic2?: string; }
options.topic2?string

Returns

StatementChannel<T>

Type Parameters

Type Parameter
T

Properties

ready

ready: boolean;

values

values: ReadonlyMap<string, T>;

Latest value per channel name (last-write-wins).


write

write: (channelName, value) => Promise<boolean>;
Parameters
ParameterType
channelNamestring
valueT
Returns

Promise<boolean>

Vue

function useStatementChannel<T>(options?): StatementChannel<T>;

Last-write-wins channels (presence, live cursors, ephemeral app state). ready stays false standalone.

Type Parameters

Type Parameter
T extends object

Parameters

ParameterType
options?{ topic2?: string; }
options.topic2?string

Returns

StatementChannel<T>

Type Parameters

Type Parameter
T

Properties

ready

ready: Ref<boolean>;

values

values: Ref<ReadonlyMap<string, T>>;

Latest value per channel name (last-write-wins).


write

write: (channelName, value) => Promise<boolean>;
Parameters
ParameterType
channelNamestring
valueT
Returns

Promise<boolean>

On this page