/** * AdminShell activity feed adapter — bridge a `@happyvertical/smrt-web` live * collection into the AdminShell activity registry (#1779, part of the WASD * AdminShell epic #1766). * * This is a thin, OPT-IN CONSUMER of already-shipped infrastructure, not a new * generated surface. It sits at the intersection of two existing contracts: * * - the runes-reactive {@link liveCollection} view over a `SmrtWebCollection` * (`@happyvertical/smrt-svelte/web`, slice A of #1761), and * - the `ShellState` activity registry (`upsertActivity` / `updateActivity` / * `removeActivity`, from `components/workspace/admin-shell`). * * The app supplies (a) the `@smrt()` DOMAIN collection (a `SmrtWebCollection`) * and (b) an editorial {@link ActivityFeedMap} that turns each row into the * shell-facing activity fields (`kind`, `scope`, `label`, `progress`, …). The * adapter reconciles the mapped rows against the shell as rows appear, change, * and vanish, and returns a disposer that removes the activities it created. * * ── Why it lives behind the `/web` opt-in entry ───────────────────────────── * `liveCollection` pulls the client-data engine (`@tanstack/db` + * `@tanstack/svelte-db`). Keeping this adapter in the `/web` subpath keeps that * engine OUT of the AdminShell core: nothing under `components/workspace/` may * import this module, so `@happyvertical/smrt-svelte/workspace` stays * transport-agnostic and TanStack-free (a hard constraint of #1766). The app * wires the adapter at the edge, next to where it already opts into `/web`. * * ── Engine-absorption boundary ────────────────────────────────────────────── * No `@tanstack/*` type appears here. The adapter reconciles against the plain * DTO rows exposed by {@link liveCollection} (which already strips the engine's * `$`-prefixed virtual props), so the engine stays swappable behind the same * boundary the rest of `/web` respects. * * ── Reconciliation, not change-diffing ────────────────────────────────────── * Rather than decode the engine's change payload, the adapter re-derives the * activity set from the current live rows on every reactive tick (via an * internal `$effect` over `view.rows`) and diffs it against what it last pushed * to the shell. This is robust to the change-notification shape (which is * engine-internal) and mirrors how the runtime's own consumers treat a change * as a "something moved, re-read" signal. */ import type { SmrtWebCollection, SmrtWebRow } from '@happyvertical/smrt-web'; import type { ShellState } from '../components/workspace/admin-shell/state.svelte.js'; import type { ShellActivity } from '../components/workspace/admin-shell/types.js'; /** * The shell-facing fields an {@link ActivityFeedMap} produces for one row — the * editorial half of a {@link ShellActivity}. The adapter owns the bookkeeping * fields (`id`, `createdAt`, `updatedAt`), so they are omitted here; `id` may be * supplied to decouple the activity id from the row id (it defaults to the row's * `id`). Everything a consumer legitimately controls (`kind`, `scope`, `label`, * `status`, `subject`, `progress`, `detailHref`, `message`, `edge`, `cancel`) * passes straight through. */ export type ShellActivityInput = Omit & { /** * Explicit activity id. Defaults to the row's `id` when omitted — the common * case (one activity per row). Supply it only to map a row onto a * differently-keyed activity. */ id?: string; }; /** * Map one live-collection row to its shell activity fields, or `null` to * exclude the row from the feed. * * Returning `null` is a first-class signal: a row that maps to `null` is NOT an * activity (e.g. a draft the shell should ignore), and a row that STOPS mapping * to an activity — flips from a value to `null` — is removed from the shell, * exactly as if it had vanished from the collection. This lets the editorial map * gate which rows surface without the consumer filtering the collection. * * @typeParam TData - the row DTO shape carried by the collection. */ export type ActivityFeedMap = (row: SmrtWebRow) => ShellActivityInput | null; /** * Options for {@link activityFeed}. * * @typeParam TData - the row DTO shape carried by {@link collection}. */ export interface ActivityFeedOptions { /** * The domain live collection to bridge — a `SmrtWebCollection` from * `@happyvertical/smrt-web` (e.g. built by the app's `createSmrtCollection` * over its `@smrt()` class). Its rows drive the feed. */ collection: SmrtWebCollection; /** * The editorial mapping from a row to its shell activity fields (or `null` to * exclude the row). See {@link ActivityFeedMap}. */ map: ActivityFeedMap; /** The AdminShell state whose activity registry this feed drives. */ shell: ShellState; /** * Forwarded to the underlying {@link liveCollection}: trigger the collection's * first load eagerly when the feed is created. Defaults to `true`. */ preload?: boolean; } /** * A handle to a running {@link activityFeed}. Call {@link dispose} to detach the * feed and remove every activity it created; read {@link isDisposed} to check. */ export interface ActivityFeedHandle { /** * Detach the feed: remove every activity this feed still owns from the shell * and stop reconciling. Idempotent. The underlying live-query subscription is * torn down by Svelte when the hosting component unmounts (that is where * `activityFeed` must be called); call this to remove the feed's activities * sooner, or when tearing a feed down without unmounting. */ dispose(): void; /** True once {@link dispose} has run. */ readonly isDisposed: boolean; } /** * The pure reconciliation core of {@link activityFeed}: it owns the diff between * a set of live rows and the shell's activity registry, with NO Svelte reactive * or client-data-engine dependency. {@link activityFeed} wraps one of these in a * `$effect` over a {@link liveCollection} view; this split keeps the mapping / * create-update-remove / ownership logic testable against a real `ShellState` * with plain row arrays (mock only externals — see the `__tests__`). * * @internal Not part of the public surface — use {@link activityFeed}. * @typeParam TData - the row DTO shape being reconciled. */ export declare class ActivityFeedReconciler { private readonly shell; private readonly map; /** * Activities this feed currently owns, keyed by activity id, each with the * last resolved input + fingerprint so a reconcile can tell created / changed * / unchanged apart and remove exactly its own set on teardown. */ private readonly owned; private disposed; constructor(shell: ShellState, map: ActivityFeedMap); /** True once {@link dispose} has run. */ get isDisposed(): boolean; /** * Reconcile `rows` against the owned activity set: upsert newly-appearing * activities, update changed ones, remove vanished ones. A pure diff — an * unchanged tick (same rows, same mappings) performs ZERO shell mutations. * No-op once disposed. */ reconcile(rows: ReadonlyArray>): void; /** * Retract exactly this feed's activities from the shell and stop reconciling. * Idempotent. Activities owned by other sources are left untouched. */ dispose(): void; } /** * Bridge a `@happyvertical/smrt-web` live collection into an AdminShell's * activity registry: each mapped row becomes a {@link ShellActivity} that * appears, updates, and disappears in the shell as the collection changes. * * MUST be called during Svelte component initialization — it delegates to * {@link liveCollection} (which installs a `$effect`) and installs its own * reconciliation `$effect`. Both bind to the calling component's lifecycle, so * the live subscription and reconciliation tear down automatically on unmount. * Unmount ALSO auto-retracts the feed's activities from the shell (via an * `$effect` teardown), so they never linger after the host component is gone — * no manual cleanup required. The returned {@link ActivityFeedHandle.dispose} * retracts them SOONER (without unmounting) and is idempotent, so calling it and * then unmounting is safe. * * Lifecycle per row: * - a row that newly maps to an activity → `shell.upsertActivity(...)` (creates * it, stamping `createdAt`); * - an owned row whose mapping changes → `shell.updateActivity(id, patch)` * (preserves `createdAt`, bumps `updatedAt`, and the shell emits a * `transition` event when `status` changed — driving toasts); * - a row that vanishes, or whose mapping flips to `null` → * `shell.removeActivity(id)`. * * Only activities this feed created are ever touched: activities pushed to the * shell by other sources (or a second feed) are left untouched, and `dispose` * removes exactly this feed's set. * * @typeParam TData - the row DTO shape carried by {@link ActivityFeedOptions.collection}. * * @example * ```svelte * * ``` */ export declare function activityFeed(options: ActivityFeedOptions): ActivityFeedHandle; //# sourceMappingURL=activity-feed.svelte.d.ts.map