AbsoluteJS

@absolutejs/sync-pack-presence

@absolutejs/sync-pack-presencev0.4.3betaData & Sync

Per-channel live presence pack for @absolutejs/sync — heartbeat-driven, scoped, TTL-cleaned, with cursor + typing state patches

#Installation

BASH
bun add @absolutejs/sync-pack-presence

#Capabilities

Overview

Per-channel live presence for @absolutejs/sync. Heartbeat-driven, scoped (per workspace/tenant), TTL-cleaned. Plugs into a SyncEngine with one engine.registerPack(...) call.

Usage

The pack exposes:

Surface — Name — What it does

Collection — presence — Subscribe with params: { channel } — returns live members

Show 3 more

Mutation — presence:heartbeat — Upsert the caller's row in a channel and refresh its TTL

Mutation — presence:leave — Delete the caller's row in a channel

Schedule — presence:cleanup — Delete rows with expiresAt <= now (cron-fired by your host)

Storage

By default the pack uses an in-memory store — presence is ephemeral and almost always fine to lose on restart. To use a persistent backend (Drizzle, Postgres, Redis, …) pass a custom store:

Multiple instances

To run two presence packs on the same engine (e.g. one per product surface), pass a prefix to each — it scopes the owned table, the collection name, the mutation names, and the schedule name:

Composition

This pack composes via subscriptions, not cross-pack mutation calls. If another pack wants to react to presence changes (e.g. a typing-indicator display), it subscribes to the presence collection — it does not call presence:heartbeat from inside its own handler. That keeps packs decoupled.

What's in the SyncPack

createPresencePack(config) returns a plain SyncPack record:

ownsTables: ['presence'] (or [${prefix}presence])

schemas: field validators for the presence row

Show 7 more

permissions: read scoped to scope(ctx), write requires

row.actorId === getActorId(ctx)

readers / writers: the in-memory store (or your custom one)

collections: the per-channel live-members collection

mutations: presence:heartbeat and presence:leave

schedules: presence:cleanup with a retry policy

This is inspectable at runtime via engine.inspect().packs.

Outcomes

What you can build

Build on the supported package contract

Use @absolutejs/sync-pack-presence through its supported public entry points.

Hardening checklist

Production guidance

Make every external boundary explicitPin the deployed @absolutejs/sync-pack-presence version, replace example or memory-backed dependencies with durable implementations, bound external calls, protect credentials, and emit enough evidence to retry or recover safely.

Follow in order

Troubleshooting path

1
Trace from the first failed boundary
Reproduce the smallest canonical @absolutejs/sync-pack-presence example, confirm the supported entry point and version in the API explorer, then inspect the first boundary that did not produce its documented result.

#@absolutejs/sync-pack-presence quick start

Partial snippet

# @absolutejs/sync-pack-presence

BASH
bun add @absolutejs/sync-pack-presence

#Usage

Partial snippet

Working example for Usage.

TS
import { createSyncEngine } from '@absolutejs/sync/engine';
import { createPresencePack } from '@absolutejs/sync-pack-presence';

const engine = createSyncEngine();
engine.registerPack(
	createPresencePack({
		// REQUIRED in practice: how the pack reads the current actor id from
		// your app's ctx. Default is `(ctx) => ctx.userId`.
		getActorId: (ctx) => ctx.session.userId,

		// OPTIONAL: tenant/workspace scope. Two scopes never see each other's
		// presence rows.
		scope: (ctx) => ctx.session.workspaceId,

		// OPTIONAL: TTL on a heartbeat (seconds). Default 30.
		heartbeatTtlSec: 30,

		// OPTIONAL: cron for the cleanup schedule. Default every 15 seconds.
		// You must still wire `@elysiajs/cron` to fire this — sync only owns
		// the handler, not the trigger.
		cleanupCron: '*/15 * * * * *'
	})
);

#Storage

Partial snippet

By default the pack uses an in-memory store — presence is ephemeral and almost always fine to lose on restart. To use a persistent backend (Drizzle, Postgres, Redis, …) pass a custom store:

TS
import { createPresencePack, type PresenceStore } from '@absolutejs/sync-pack-presence';

const store: PresenceStore = {
	reader: { all: () => /* SELECT * FROM presence */ },
	writer: {
		insert: (row) => /* INSERT */,
		update: (row) => /* UPDATE */,
		delete: (row) => /* DELETE */,
	},
	expired: (now) => /* SELECT * FROM presence WHERE expires_at <= $1 */
};

engine.registerPack(createPresencePack({ store, getActorId: (ctx) => ctx.userId }));

#Multiple instances

Partial snippet

To run two presence packs on the same engine (e.g. one per product surface), pass a prefix to each — it scopes the owned table, the collection name, the mutation names, and the schedule name:

TS
engine.registerPack(createPresencePack({ prefix: 'docs_', getActorId }));
engine.registerPack(createPresencePack({ prefix: 'chat_', getActorId }));

// Mutations are now `docs_presence:heartbeat` and `chat_presence:heartbeat`.
// Collections are `docs_presence` and `chat_presence`.
// Schedules are `docs_presence:cleanup` and `chat_presence:cleanup`.

#Public entry points

Supported entry points declared by this package manifest.

Package entry point declared in package.json.

@absolutejs/sync-pack-presence@absolutejs/sync-pack-presence/manifest@absolutejs/sync-pack-presence/manifest.json

#Package commands

Scripts declared by this package manifest.

bun run buildrm -rf dist && bun build src/index.ts src/manifest.ts --outdir dist --sourcemap --target=bun --external @absolutejs/sync && tsc --project tsconfig.build.json && absolute-manifest emit
bun run formatprettier --write "./**/*.{ts,json,md}"
bun run testbun test
bun run typechecktsc --noEmit

#API reference

Search the declarations exported by the current package type files. Expand a symbol to inspect its source-backed signature.

6 symbols
PresenceRowtypePermalinkSource

Per-actor presence row stored in the pack's owned table.

TS
type PresenceRow<State = unknown> = {
    /**
     * Row identity — derived from `${channel}:${actorId}`. One row per
     * (channel, actor). Heartbeats upsert; leave deletes.
     */
    id: string;
    channel: string;
    actorId: string;
    /**
     * Scope value from the host's `scope(ctx)` config. The pack filters
     * reads/writes by this — two scopes never see each other's presence.
     */
    scope: string | null;
    /** Arbitrary payload the actor publishes (cursor pos, typing flag, etc). */
    state: State;
    /** Epoch ms after which the row is eligible for cleanup. */
    expiresAt: number;
    /** Epoch ms of the most recent heartbeat (also = expiresAt − ttl). */
    heartbeatAt: number;
};
Exported from @absolutejs/sync-pack-presence