Build on the supported package contract
Use @absolutejs/sync-pack-triage through its supported public entry points.
@absolutejs/sync-pack-triagev0.1.0betaData & SyncPer-actor triage pack for @absolutejs/sync — unread/seen, snooze that resurfaces on new activity, dismiss that sticks, and mute, over any host resource
bun add @absolutejs/sync-pack-triagePer-actor triage state for @absolutejs/sync: unread/seen, snooze, dismiss, and mute over any resource your app already owns.
Every product with a backlog — an inbox, a task list, a review queue, a deal room — grows the same four controls, and every product rebuilds them. This pack owns one triage table keyed by (actorId, resourceKind, resourceId) and gives you the mutations and the owner-scoped live collection to drive them.
Dismiss sticks. New activity does not resurrect something the actor put away. A "dismiss" that quietly means "hide until it changes" brings the item back at the worst possible moment, and the actor stops trusting the control. Only triage:restore undoes a dismiss.
Snooze resurfaces. A snoozed item comes back the moment the other side acts — not when its timer happens to expire. Waiting out a three-day snooze while a reply sits hidden is how a deal dies. This is the Gmail norm and the one people expect.
Both live in one pure function, so you can apply the same rule inside your own SQL-backed list query without instantiating the pack:
Pass lastActivityAt and a snooze ends early when the resource changes. Omit it and a snooze ends only on its timer.
Mutation — Args — Effect
triage:seen — {resourceKind, resourceId} — Stamps lastSeenAt
triage:unread — {resourceKind, resourceId} — Clears lastSeenAt
triage:snooze — {resourceKind, resourceId, snoozedUntil?} — status: 'snoozed'; snoozedUntil: null sleeps until new activity
triage:dismiss — {resourceKind, resourceId} — status: 'dismissed', stamps dismissedAt
triage:restore — {resourceKind, resourceId} — Back to status: 'active'
triage:mute / triage:unmute — {resourceKind, resourceId} — Stop/resume notifying, without putting it away
triage:bulk — {action, resources[], snoozedUntil?} — One round-trip, one diff, for a multi-select
Row ids are ${actorId}:${resourceKind}:${resourceId}, so every mutation is an idempotent upsert and a row's owner is recoverable from its key. Read and write permissions are owner-scoped and enforced against the stored row, not the client's payload.
store is three functions — all, getById, and a TableWriter — and both reads may be async, so you can back triage with your real database rather than a mirror.
The wakeCron sweep only exists to turn an expired snooze into a live change event. Reads are already correct without it, because triageVisibility resolves expiry at read time; a host that hasn't wired the scheduled plugin still behaves correctly, just without a push at the moment of expiry.
{ resourceKind?, status? }. Both are optional; status is resolved with triageVisibility at the current time and without a per-row activity timestamp, because this pack owns triage state and not your activity feed. If you want activity-aware resurfacing inside a live query, join this table in your own collection and call triageVisibility there with your own timestamp.
Outcomes
Use @absolutejs/sync-pack-triage through its supported public entry points.
Hardening checklist
Follow in order
# @absolutejs/sync-pack-triage
bun add @absolutejs/sync-pack-triage# @absolutejs/sync-pack-triage
import { createSyncEngine } from "@absolutejs/sync/engine";
import { createTriagePack } from "@absolutejs/sync-pack-triage";
const engine = createSyncEngine();
engine.registerPack(createTriagePack({ getActorId: (ctx) => ctx.userId }));
await engine.runMutation(
"triage:snooze",
{
resourceId: "thread-9",
resourceKind: "thread",
snoozedUntil: Date.now() + 86_400_000,
},
{ userId: "alice" },
);
const view = await engine.subscribe({
collection: "triage",
ctx: { userId: "alice" },
onDiff: (diff) => render(diff),
params: { resourceKind: "thread", status: "active" },
});Dismiss sticks. New activity does not resurrect something the actor put away. A "dismiss" that quietly means "hide until it changes" brings the item back at the worst possible moment, and the actor stops trusting the control. Only triage:restore undoes a dismiss.
import { triageVisibility, isUnread } from "@absolutejs/sync-pack-triage";
triageVisibility(row, { lastActivityAt: thread.lastMessageAt }); // 'active' | 'snoozed' | 'dismissed'
isUnread(row, thread.lastMessageAt); // never looked, or changed since they didWorking example for Config.
createTriagePack({
getActorId: (ctx) => ctx.userId, // default reads ctx.userId
prefix: "team_", // namespaces table, collection, mutations, schedule
store: myPostgresTriageStore, // default is per-instance in-memory
now: () => Date.now(),
maxBulkSize: 500,
wakeCron: "*/5 * * * *", // null to skip registering the sweep
});Supported entry points declared by this package manifest.
Package entry point declared in package.json.
Scripts declared by this package manifest.
Search the declarations exported by the current package type files. Expand a symbol to inspect its source-backed signature.