Build on the supported package contract
Use @absolutejs/sync-pack-favorites through its supported public entry points.
@absolutejs/sync-pack-favoritesv0.4.1betaData & SyncPer-actor saved-resources pack for @absolutejs/sync — favorite/unfavorite/pin/list with optional join to host resources
bun add @absolutejs/sync-pack-favoritesPer-actor saved-resources pack for @absolutejs/sync. Each actor sees only their own rows; favoriting is idempotent (deterministic row id); optional join collection pairs each favorite with the host's resource row in one live subscription.
Then from the client:
Surface — Name — What it does
Collection — favorites — Per-actor list. Optional params.resourceKind filter
Mutation — favorites:favorite — Idempotent insert. Subsequent calls are no-ops
Mutation — favorites:unfavorite — Idempotent delete. No-op if the row never existed
Mutation — favorites:toggle — Insert if missing, delete if present. Returns { favorited }
Collection — favorites-with-resource (opt) — Join with the host's resource table — see below
The deterministic id means the same (actor, kind, resource) triple always maps to the same row — so duplicate favorite calls are idempotent at the storage layer, not just behaviorally.
When you set joinResources, the pack additionally registers a join collection that pairs each favorite with the host's resource row (same pattern as comments-with-author from sync-pack-comments).
Inner join — favorites whose resource has been deleted drop out of the join (but stay in the base favorites collection).
Default: per-instance in-memory store. For a persistent backend, pass a custom store:
Pass a prefix to coexist with another favorites pack instance (e.g. a "team" set vs a "private" set):
Outcomes
Use @absolutejs/sync-pack-favorites through its supported public entry points.
Hardening checklist
Follow in order
# @absolutejs/sync-pack-favorites
bun add @absolutejs/sync-pack-favoritesWorking example for Usage.
import { createSyncEngine } from '@absolutejs/sync/engine';
import { createFavoritesPack } from '@absolutejs/sync-pack-favorites';
const engine = createSyncEngine();
engine.registerPack(
createFavoritesPack({
getActorId: (ctx) => ctx.session.userId,
}),
);Then from the client:
// Toggle is the easy default — one round-trip, returns the new state.
const { favorited } = await store.mutate({
args: { resourceKind: 'doc', resourceId: 'doc-123' },
name: 'favorites:toggle',
});
// Or favorite / unfavorite explicitly.
await store.mutate({ args: { resourceKind: 'doc', resourceId: 'doc-123' }, name: 'favorites:favorite' });
await store.mutate({ args: { resourceKind: 'doc', resourceId: 'doc-123' }, name: 'favorites:unfavorite' });Working example for Row shape.
type FavoriteRow = {
id: string; // `${actorId}:${resourceKind}:${resourceId}` — deterministic
actorId: string;
resourceKind: string; // app-level: "doc" | "task" | "issue" | ...
resourceId: string;
createdAt: number;
};When you set joinResources, the pack additionally registers a join collection that pairs each favorite with the host's resource row (same pattern as comments-with-author from sync-pack-comments).
type DocRow = { id: string; title: string };
engine.registerReader('docs', { all: () => db.docs.list() });
engine.registerPack(
createFavoritesPack<MyCtx, DocRow>({
getActorId,
joinResources: {
table: 'docs',
// Default (row) => row.id; override if your resource id key isn't `id`.
// key: (doc) => doc.docId,
// Required: host supplies the resource-side hydrate.
hydrate: () => db.docs.list(),
},
}),
);
const view = await engine.subscribe<
FavoriteRow & { resource: DocRow },
{ resourceKind?: string }
>({
collection: 'favorites-with-resource',
params: { resourceKind: 'doc' },
ctx,
onDiff: rerender,
});
// view.initial[0] === { ...favorite, resource: { id, title } }Supported entry points declared by this package manifest.
Package entry point declared in package.json.
Scripts declared by this package manifest.