@happyvertical/smrt-svelte 0.38.1 → 0.38.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +17 -0
- package/dist/components/workspace/NavTree.svelte +1 -1
- package/dist/components/workspace/__tests__/system-feed.test.js +327 -0
- package/dist/components/workspace/live/index.d.ts +17 -0
- package/dist/components/workspace/live/index.d.ts.map +1 -0
- package/dist/components/workspace/live/index.js +16 -0
- package/dist/components/workspace/live/system-feed.recipe.md +169 -0
- package/dist/components/workspace/live/system-feed.svelte.d.ts +129 -0
- package/dist/components/workspace/live/system-feed.svelte.d.ts.map +1 -0
- package/dist/components/workspace/live/system-feed.svelte.js +201 -0
- package/dist/i18n/strings.workspace.d.ts +26 -0
- package/dist/i18n/strings.workspace.d.ts.map +1 -1
- package/dist/i18n/strings.workspace.js +28 -0
- package/dist/web/__tests__/activity-feed-harness.svelte +32 -0
- package/dist/web/__tests__/activity-feed-harness.svelte.d.ts +18 -0
- package/dist/web/__tests__/activity-feed-harness.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/activity-feed.integration.svelte.test.js +189 -0
- package/dist/web/__tests__/activity-feed.svelte.test.js +235 -0
- package/dist/web/activity-feed.svelte.d.ts +210 -0
- package/dist/web/activity-feed.svelte.d.ts.map +1 -0
- package/dist/web/activity-feed.svelte.js +302 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +6 -0
- package/package.json +13 -7
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the AdminShell activity feed adapter's reconciliation core
|
|
3
|
+
* (#1779).
|
|
4
|
+
*
|
|
5
|
+
* These exercise {@link ActivityFeedReconciler} — the pure diff between a set of
|
|
6
|
+
* live rows and the shell activity registry — against a REAL {@link ShellState}.
|
|
7
|
+
* Per repo policy nothing here is mocked: the shell, its activity events, and
|
|
8
|
+
* the mapping are all real; the reconciler is fed plain row arrays that stand in
|
|
9
|
+
* for successive live-collection ticks (`liveCollection`'s engine is exercised
|
|
10
|
+
* separately in `activity-feed.integration.svelte.test.ts`). This isolates the
|
|
11
|
+
* mapping / create-update-remove / ownership behavior the adapter guarantees.
|
|
12
|
+
*/
|
|
13
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
14
|
+
import { createShellState } from '../../components/workspace/admin-shell/state.svelte.js';
|
|
15
|
+
import { ActivityFeedReconciler, } from '../activity-feed.svelte.js';
|
|
16
|
+
/** Editorial map: a job row → focus-scope activity, or `null` for a draft. */
|
|
17
|
+
const mapJob = (row) => {
|
|
18
|
+
if (row.draft)
|
|
19
|
+
return null;
|
|
20
|
+
return {
|
|
21
|
+
kind: 'render-job',
|
|
22
|
+
scope: 'focus',
|
|
23
|
+
label: row.title,
|
|
24
|
+
status: row.state,
|
|
25
|
+
progress: row.progress ?? null,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/** Record every activity event the shell emits, for sequence assertions. */
|
|
29
|
+
function recordEvents(shell) {
|
|
30
|
+
const events = [];
|
|
31
|
+
const stop = shell.watchActivities((event) => events.push(event));
|
|
32
|
+
return { events, stop };
|
|
33
|
+
}
|
|
34
|
+
describe('ActivityFeedReconciler', () => {
|
|
35
|
+
const teardown = [];
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
for (const fn of teardown.splice(0))
|
|
38
|
+
fn();
|
|
39
|
+
});
|
|
40
|
+
function setup() {
|
|
41
|
+
const shell = createShellState();
|
|
42
|
+
const recorded = recordEvents(shell);
|
|
43
|
+
teardown.push(recorded.stop);
|
|
44
|
+
const reconciler = new ActivityFeedReconciler(shell, mapJob);
|
|
45
|
+
return { shell, reconciler, events: recorded.events };
|
|
46
|
+
}
|
|
47
|
+
it('upserts a shell activity when a row first appears', () => {
|
|
48
|
+
const { shell, reconciler, events } = setup();
|
|
49
|
+
reconciler.reconcile([
|
|
50
|
+
{ id: 'job-1', title: 'Render intro', state: 'running', progress: 20 },
|
|
51
|
+
]);
|
|
52
|
+
const activities = shell.listActivities();
|
|
53
|
+
expect(activities).toHaveLength(1);
|
|
54
|
+
expect(activities[0]).toMatchObject({
|
|
55
|
+
id: 'job-1',
|
|
56
|
+
kind: 'render-job',
|
|
57
|
+
scope: 'focus',
|
|
58
|
+
label: 'Render intro',
|
|
59
|
+
status: 'running',
|
|
60
|
+
progress: 20,
|
|
61
|
+
});
|
|
62
|
+
// The activity id defaults to the row id, and the shell stamped createdAt.
|
|
63
|
+
expect(activities[0].createdAt).toBeTruthy();
|
|
64
|
+
// A single create event (upsert), no transition/remove.
|
|
65
|
+
expect(events.map((e) => e.type)).toEqual(['upsert']);
|
|
66
|
+
});
|
|
67
|
+
it('updates the activity in place when a row changes (status transition)', () => {
|
|
68
|
+
const { shell, reconciler, events } = setup();
|
|
69
|
+
reconciler.reconcile([
|
|
70
|
+
{ id: 'job-1', title: 'Render intro', state: 'running', progress: 20 },
|
|
71
|
+
]);
|
|
72
|
+
const createdAt = shell.listActivities()[0].createdAt;
|
|
73
|
+
// Same row id, advanced progress + status flip → an UPDATE, not a second
|
|
74
|
+
// create, and the shell reports it as a `transition` (status changed).
|
|
75
|
+
reconciler.reconcile([
|
|
76
|
+
{ id: 'job-1', title: 'Render intro', state: 'completed', progress: 100 },
|
|
77
|
+
]);
|
|
78
|
+
const activities = shell.listActivities();
|
|
79
|
+
expect(activities).toHaveLength(1);
|
|
80
|
+
expect(activities[0]).toMatchObject({
|
|
81
|
+
id: 'job-1',
|
|
82
|
+
status: 'completed',
|
|
83
|
+
progress: 100,
|
|
84
|
+
});
|
|
85
|
+
// createdAt preserved across the update (update went through updateActivity,
|
|
86
|
+
// not a fresh upsert that would re-stamp it).
|
|
87
|
+
expect(activities[0].createdAt).toBe(createdAt);
|
|
88
|
+
expect(events.map((e) => e.type)).toEqual(['upsert', 'transition']);
|
|
89
|
+
});
|
|
90
|
+
it('performs no shell mutation when a reconcile tick is unchanged', () => {
|
|
91
|
+
const { reconciler, events } = setup();
|
|
92
|
+
const rows = [
|
|
93
|
+
{ id: 'job-1', title: 'Render intro', state: 'running', progress: 20 },
|
|
94
|
+
];
|
|
95
|
+
reconciler.reconcile(rows);
|
|
96
|
+
// Re-feed identical content (a common no-op reactive tick): a new array with
|
|
97
|
+
// an equal-but-distinct row object. Must not emit a second event.
|
|
98
|
+
reconciler.reconcile([{ ...rows[0] }]);
|
|
99
|
+
expect(events.map((e) => e.type)).toEqual(['upsert']);
|
|
100
|
+
});
|
|
101
|
+
it('removes the activity when its row vanishes from the collection', () => {
|
|
102
|
+
const { shell, reconciler, events } = setup();
|
|
103
|
+
reconciler.reconcile([
|
|
104
|
+
{ id: 'job-1', title: 'Render intro', state: 'running' },
|
|
105
|
+
{ id: 'job-2', title: 'Render outro', state: 'queued' },
|
|
106
|
+
]);
|
|
107
|
+
expect(shell.listActivities()).toHaveLength(2);
|
|
108
|
+
// job-1 gone from the next tick → removed; job-2 unchanged → left alone.
|
|
109
|
+
reconciler.reconcile([
|
|
110
|
+
{ id: 'job-2', title: 'Render outro', state: 'queued' },
|
|
111
|
+
]);
|
|
112
|
+
const remaining = shell.listActivities();
|
|
113
|
+
expect(remaining).toHaveLength(1);
|
|
114
|
+
expect(remaining[0].id).toBe('job-2');
|
|
115
|
+
expect(events.map((e) => e.type)).toEqual([
|
|
116
|
+
'upsert', // job-1 create
|
|
117
|
+
'upsert', // job-2 create
|
|
118
|
+
'remove', // job-1 removed
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
it('treats a row that maps to null as absent (excluded, then removed when it flips)', () => {
|
|
122
|
+
const { shell, reconciler } = setup();
|
|
123
|
+
// A draft row maps to null → never becomes an activity.
|
|
124
|
+
reconciler.reconcile([
|
|
125
|
+
{ id: 'job-1', title: 'Draft', state: 'queued', draft: true },
|
|
126
|
+
]);
|
|
127
|
+
expect(shell.listActivities()).toHaveLength(0);
|
|
128
|
+
// It leaves draft → now an activity appears.
|
|
129
|
+
reconciler.reconcile([
|
|
130
|
+
{ id: 'job-1', title: 'Published', state: 'running' },
|
|
131
|
+
]);
|
|
132
|
+
expect(shell.listActivities()).toHaveLength(1);
|
|
133
|
+
// It goes back to draft (maps to null again) → activity is removed.
|
|
134
|
+
reconciler.reconcile([
|
|
135
|
+
{ id: 'job-1', title: 'Published', state: 'running', draft: true },
|
|
136
|
+
]);
|
|
137
|
+
expect(shell.listActivities()).toHaveLength(0);
|
|
138
|
+
});
|
|
139
|
+
it('lets the map decouple the activity id from the row id', () => {
|
|
140
|
+
const shell = createShellState();
|
|
141
|
+
const reconciler = new ActivityFeedReconciler(shell, (row) => ({
|
|
142
|
+
id: `activity:${row.id}`,
|
|
143
|
+
kind: 'render-job',
|
|
144
|
+
scope: 'system',
|
|
145
|
+
label: row.title,
|
|
146
|
+
status: row.state,
|
|
147
|
+
}));
|
|
148
|
+
reconciler.reconcile([{ id: 'job-1', title: 'Job', state: 'running' }]);
|
|
149
|
+
const activities = shell.listActivities();
|
|
150
|
+
expect(activities).toHaveLength(1);
|
|
151
|
+
expect(activities[0].id).toBe('activity:job-1');
|
|
152
|
+
});
|
|
153
|
+
it('routes an activity to the shell edge for its scope', () => {
|
|
154
|
+
const { shell, reconciler } = setup();
|
|
155
|
+
reconciler.reconcile([{ id: 'job-1', title: 'Job', state: 'running' }]);
|
|
156
|
+
// scope 'focus' homes to the right edge (SCOPE_EDGES.focus === 'right').
|
|
157
|
+
expect(shell.listActivities({ edge: 'right' })).toHaveLength(1);
|
|
158
|
+
});
|
|
159
|
+
it('re-homes the activity to the new edge when its scope changes (no explicit edge)', () => {
|
|
160
|
+
const shell = createShellState();
|
|
161
|
+
// Map keys scope off the row so a later tick can change it; no `edge` set.
|
|
162
|
+
const reconciler = new ActivityFeedReconciler(shell, (row) => ({
|
|
163
|
+
kind: 'render-job',
|
|
164
|
+
// Reuse the unused `draft` flag as a cheap "move to system scope" toggle.
|
|
165
|
+
scope: row.draft ? 'system' : 'focus',
|
|
166
|
+
label: row.title,
|
|
167
|
+
status: row.state,
|
|
168
|
+
}));
|
|
169
|
+
reconciler.reconcile([{ id: 'job-1', title: 'Job', state: 'running' }]);
|
|
170
|
+
// Focus → right edge.
|
|
171
|
+
expect(shell.listActivities({ edge: 'right' })).toHaveLength(1);
|
|
172
|
+
expect(shell.listActivities({ edge: 'bottom' })).toHaveLength(0);
|
|
173
|
+
// Scope flips to system; the map still sets no explicit edge. The activity
|
|
174
|
+
// must RE-DERIVE its edge from the new scope (system → bottom), not stay
|
|
175
|
+
// pinned to the old right edge.
|
|
176
|
+
reconciler.reconcile([
|
|
177
|
+
{ id: 'job-1', title: 'Job', state: 'running', draft: true },
|
|
178
|
+
]);
|
|
179
|
+
expect(shell.listActivities({ edge: 'right' })).toHaveLength(0);
|
|
180
|
+
expect(shell.listActivities({ edge: 'bottom' })).toHaveLength(1);
|
|
181
|
+
expect(shell.listActivities()[0].scope).toBe('system');
|
|
182
|
+
});
|
|
183
|
+
it('clears a previously-set optional field when a later mapping drops it', () => {
|
|
184
|
+
const shell = createShellState();
|
|
185
|
+
// Map emits `message`/`detailHref` only while the job is running.
|
|
186
|
+
const reconciler = new ActivityFeedReconciler(shell, (row) => ({
|
|
187
|
+
kind: 'render-job',
|
|
188
|
+
scope: 'focus',
|
|
189
|
+
label: row.title,
|
|
190
|
+
status: row.state,
|
|
191
|
+
...(row.state === 'running'
|
|
192
|
+
? { message: 'Rendering frames', detailHref: '/jobs/job-1' }
|
|
193
|
+
: {}),
|
|
194
|
+
}));
|
|
195
|
+
reconciler.reconcile([{ id: 'job-1', title: 'Job', state: 'running' }]);
|
|
196
|
+
let activity = shell.listActivities()[0];
|
|
197
|
+
expect(activity.message).toBe('Rendering frames');
|
|
198
|
+
expect(activity.detailHref).toBe('/jobs/job-1');
|
|
199
|
+
// The job completes → the map no longer emits message/detailHref. Those
|
|
200
|
+
// stale values must be CLEARED, not retained from the previous mapping.
|
|
201
|
+
reconciler.reconcile([{ id: 'job-1', title: 'Job', state: 'completed' }]);
|
|
202
|
+
activity = shell.listActivities()[0];
|
|
203
|
+
expect(activity.status).toBe('completed');
|
|
204
|
+
expect(activity.message).toBeUndefined();
|
|
205
|
+
expect(activity.detailHref).toBeUndefined();
|
|
206
|
+
});
|
|
207
|
+
it('dispose() removes exactly the feed activities and stops reconciling', () => {
|
|
208
|
+
const { shell, reconciler, events } = setup();
|
|
209
|
+
// An activity from ANOTHER source the feed must never touch.
|
|
210
|
+
shell.upsertActivity({
|
|
211
|
+
id: 'external-1',
|
|
212
|
+
label: 'External',
|
|
213
|
+
kind: 'other',
|
|
214
|
+
scope: 'app',
|
|
215
|
+
status: 'running',
|
|
216
|
+
});
|
|
217
|
+
reconciler.reconcile([
|
|
218
|
+
{ id: 'job-1', title: 'Render intro', state: 'running' },
|
|
219
|
+
]);
|
|
220
|
+
expect(shell.listActivities()).toHaveLength(2);
|
|
221
|
+
reconciler.dispose();
|
|
222
|
+
// Only the feed's own activity was retracted; the external one survives.
|
|
223
|
+
const remaining = shell.listActivities();
|
|
224
|
+
expect(remaining).toHaveLength(1);
|
|
225
|
+
expect(remaining[0].id).toBe('external-1');
|
|
226
|
+
expect(reconciler.isDisposed).toBe(true);
|
|
227
|
+
// Reconciling after dispose is a no-op (no new activities, no events).
|
|
228
|
+
const eventsBefore = events.length;
|
|
229
|
+
reconciler.reconcile([
|
|
230
|
+
{ id: 'job-2', title: 'Late job', state: 'running' },
|
|
231
|
+
]);
|
|
232
|
+
expect(shell.listActivities()).toHaveLength(1);
|
|
233
|
+
expect(events.length).toBe(eventsBefore);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdminShell activity feed adapter — bridge a `@happyvertical/smrt-web` live
|
|
3
|
+
* collection into the AdminShell activity registry (#1779, part of the WASD
|
|
4
|
+
* AdminShell epic #1766).
|
|
5
|
+
*
|
|
6
|
+
* This is a thin, OPT-IN CONSUMER of already-shipped infrastructure, not a new
|
|
7
|
+
* generated surface. It sits at the intersection of two existing contracts:
|
|
8
|
+
*
|
|
9
|
+
* - the runes-reactive {@link liveCollection} view over a `SmrtWebCollection`
|
|
10
|
+
* (`@happyvertical/smrt-svelte/web`, slice A of #1761), and
|
|
11
|
+
* - the `ShellState` activity registry (`upsertActivity` / `updateActivity` /
|
|
12
|
+
* `removeActivity`, from `components/workspace/admin-shell`).
|
|
13
|
+
*
|
|
14
|
+
* The app supplies (a) the `@smrt()` DOMAIN collection (a `SmrtWebCollection`)
|
|
15
|
+
* and (b) an editorial {@link ActivityFeedMap} that turns each row into the
|
|
16
|
+
* shell-facing activity fields (`kind`, `scope`, `label`, `progress`, …). The
|
|
17
|
+
* adapter reconciles the mapped rows against the shell as rows appear, change,
|
|
18
|
+
* and vanish, and returns a disposer that removes the activities it created.
|
|
19
|
+
*
|
|
20
|
+
* ── Why it lives behind the `/web` opt-in entry ─────────────────────────────
|
|
21
|
+
* `liveCollection` pulls the client-data engine (`@tanstack/db` +
|
|
22
|
+
* `@tanstack/svelte-db`). Keeping this adapter in the `/web` subpath keeps that
|
|
23
|
+
* engine OUT of the AdminShell core: nothing under `components/workspace/` may
|
|
24
|
+
* import this module, so `@happyvertical/smrt-svelte/workspace` stays
|
|
25
|
+
* transport-agnostic and TanStack-free (a hard constraint of #1766). The app
|
|
26
|
+
* wires the adapter at the edge, next to where it already opts into `/web`.
|
|
27
|
+
*
|
|
28
|
+
* ── Engine-absorption boundary ──────────────────────────────────────────────
|
|
29
|
+
* No `@tanstack/*` type appears here. The adapter reconciles against the plain
|
|
30
|
+
* DTO rows exposed by {@link liveCollection} (which already strips the engine's
|
|
31
|
+
* `$`-prefixed virtual props), so the engine stays swappable behind the same
|
|
32
|
+
* boundary the rest of `/web` respects.
|
|
33
|
+
*
|
|
34
|
+
* ── Reconciliation, not change-diffing ──────────────────────────────────────
|
|
35
|
+
* Rather than decode the engine's change payload, the adapter re-derives the
|
|
36
|
+
* activity set from the current live rows on every reactive tick (via an
|
|
37
|
+
* internal `$effect` over `view.rows`) and diffs it against what it last pushed
|
|
38
|
+
* to the shell. This is robust to the change-notification shape (which is
|
|
39
|
+
* engine-internal) and mirrors how the runtime's own consumers treat a change
|
|
40
|
+
* as a "something moved, re-read" signal.
|
|
41
|
+
*/
|
|
42
|
+
import type { SmrtWebCollection, SmrtWebRow } from '@happyvertical/smrt-web';
|
|
43
|
+
import type { ShellState } from '../components/workspace/admin-shell/state.svelte.js';
|
|
44
|
+
import type { ShellActivity } from '../components/workspace/admin-shell/types.js';
|
|
45
|
+
/**
|
|
46
|
+
* The shell-facing fields an {@link ActivityFeedMap} produces for one row — the
|
|
47
|
+
* editorial half of a {@link ShellActivity}. The adapter owns the bookkeeping
|
|
48
|
+
* fields (`id`, `createdAt`, `updatedAt`), so they are omitted here; `id` may be
|
|
49
|
+
* supplied to decouple the activity id from the row id (it defaults to the row's
|
|
50
|
+
* `id`). Everything a consumer legitimately controls (`kind`, `scope`, `label`,
|
|
51
|
+
* `status`, `subject`, `progress`, `detailHref`, `message`, `edge`, `cancel`)
|
|
52
|
+
* passes straight through.
|
|
53
|
+
*/
|
|
54
|
+
export type ShellActivityInput = Omit<ShellActivity, 'id' | 'createdAt' | 'updatedAt'> & {
|
|
55
|
+
/**
|
|
56
|
+
* Explicit activity id. Defaults to the row's `id` when omitted — the common
|
|
57
|
+
* case (one activity per row). Supply it only to map a row onto a
|
|
58
|
+
* differently-keyed activity.
|
|
59
|
+
*/
|
|
60
|
+
id?: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Map one live-collection row to its shell activity fields, or `null` to
|
|
64
|
+
* exclude the row from the feed.
|
|
65
|
+
*
|
|
66
|
+
* Returning `null` is a first-class signal: a row that maps to `null` is NOT an
|
|
67
|
+
* activity (e.g. a draft the shell should ignore), and a row that STOPS mapping
|
|
68
|
+
* to an activity — flips from a value to `null` — is removed from the shell,
|
|
69
|
+
* exactly as if it had vanished from the collection. This lets the editorial map
|
|
70
|
+
* gate which rows surface without the consumer filtering the collection.
|
|
71
|
+
*
|
|
72
|
+
* @typeParam TData - the row DTO shape carried by the collection.
|
|
73
|
+
*/
|
|
74
|
+
export type ActivityFeedMap<TData extends object> = (row: SmrtWebRow<TData>) => ShellActivityInput | null;
|
|
75
|
+
/**
|
|
76
|
+
* Options for {@link activityFeed}.
|
|
77
|
+
*
|
|
78
|
+
* @typeParam TData - the row DTO shape carried by {@link collection}.
|
|
79
|
+
*/
|
|
80
|
+
export interface ActivityFeedOptions<TData extends object> {
|
|
81
|
+
/**
|
|
82
|
+
* The domain live collection to bridge — a `SmrtWebCollection` from
|
|
83
|
+
* `@happyvertical/smrt-web` (e.g. built by the app's `createSmrtCollection`
|
|
84
|
+
* over its `@smrt()` class). Its rows drive the feed.
|
|
85
|
+
*/
|
|
86
|
+
collection: SmrtWebCollection<TData>;
|
|
87
|
+
/**
|
|
88
|
+
* The editorial mapping from a row to its shell activity fields (or `null` to
|
|
89
|
+
* exclude the row). See {@link ActivityFeedMap}.
|
|
90
|
+
*/
|
|
91
|
+
map: ActivityFeedMap<TData>;
|
|
92
|
+
/** The AdminShell state whose activity registry this feed drives. */
|
|
93
|
+
shell: ShellState;
|
|
94
|
+
/**
|
|
95
|
+
* Forwarded to the underlying {@link liveCollection}: trigger the collection's
|
|
96
|
+
* first load eagerly when the feed is created. Defaults to `true`.
|
|
97
|
+
*/
|
|
98
|
+
preload?: boolean;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A handle to a running {@link activityFeed}. Call {@link dispose} to detach the
|
|
102
|
+
* feed and remove every activity it created; read {@link isDisposed} to check.
|
|
103
|
+
*/
|
|
104
|
+
export interface ActivityFeedHandle {
|
|
105
|
+
/**
|
|
106
|
+
* Detach the feed: remove every activity this feed still owns from the shell
|
|
107
|
+
* and stop reconciling. Idempotent. The underlying live-query subscription is
|
|
108
|
+
* torn down by Svelte when the hosting component unmounts (that is where
|
|
109
|
+
* `activityFeed` must be called); call this to remove the feed's activities
|
|
110
|
+
* sooner, or when tearing a feed down without unmounting.
|
|
111
|
+
*/
|
|
112
|
+
dispose(): void;
|
|
113
|
+
/** True once {@link dispose} has run. */
|
|
114
|
+
readonly isDisposed: boolean;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The pure reconciliation core of {@link activityFeed}: it owns the diff between
|
|
118
|
+
* a set of live rows and the shell's activity registry, with NO Svelte reactive
|
|
119
|
+
* or client-data-engine dependency. {@link activityFeed} wraps one of these in a
|
|
120
|
+
* `$effect` over a {@link liveCollection} view; this split keeps the mapping /
|
|
121
|
+
* create-update-remove / ownership logic testable against a real `ShellState`
|
|
122
|
+
* with plain row arrays (mock only externals — see the `__tests__`).
|
|
123
|
+
*
|
|
124
|
+
* @internal Not part of the public surface — use {@link activityFeed}.
|
|
125
|
+
* @typeParam TData - the row DTO shape being reconciled.
|
|
126
|
+
*/
|
|
127
|
+
export declare class ActivityFeedReconciler<TData extends object> {
|
|
128
|
+
private readonly shell;
|
|
129
|
+
private readonly map;
|
|
130
|
+
/**
|
|
131
|
+
* Activities this feed currently owns, keyed by activity id, each with the
|
|
132
|
+
* last resolved input + fingerprint so a reconcile can tell created / changed
|
|
133
|
+
* / unchanged apart and remove exactly its own set on teardown.
|
|
134
|
+
*/
|
|
135
|
+
private readonly owned;
|
|
136
|
+
private disposed;
|
|
137
|
+
constructor(shell: ShellState, map: ActivityFeedMap<TData>);
|
|
138
|
+
/** True once {@link dispose} has run. */
|
|
139
|
+
get isDisposed(): boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Reconcile `rows` against the owned activity set: upsert newly-appearing
|
|
142
|
+
* activities, update changed ones, remove vanished ones. A pure diff — an
|
|
143
|
+
* unchanged tick (same rows, same mappings) performs ZERO shell mutations.
|
|
144
|
+
* No-op once disposed.
|
|
145
|
+
*/
|
|
146
|
+
reconcile(rows: ReadonlyArray<SmrtWebRow<TData>>): void;
|
|
147
|
+
/**
|
|
148
|
+
* Retract exactly this feed's activities from the shell and stop reconciling.
|
|
149
|
+
* Idempotent. Activities owned by other sources are left untouched.
|
|
150
|
+
*/
|
|
151
|
+
dispose(): void;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Bridge a `@happyvertical/smrt-web` live collection into an AdminShell's
|
|
155
|
+
* activity registry: each mapped row becomes a {@link ShellActivity} that
|
|
156
|
+
* appears, updates, and disappears in the shell as the collection changes.
|
|
157
|
+
*
|
|
158
|
+
* MUST be called during Svelte component initialization — it delegates to
|
|
159
|
+
* {@link liveCollection} (which installs a `$effect`) and installs its own
|
|
160
|
+
* reconciliation `$effect`. Both bind to the calling component's lifecycle, so
|
|
161
|
+
* the live subscription and reconciliation tear down automatically on unmount.
|
|
162
|
+
* Unmount ALSO auto-retracts the feed's activities from the shell (via an
|
|
163
|
+
* `$effect` teardown), so they never linger after the host component is gone —
|
|
164
|
+
* no manual cleanup required. The returned {@link ActivityFeedHandle.dispose}
|
|
165
|
+
* retracts them SOONER (without unmounting) and is idempotent, so calling it and
|
|
166
|
+
* then unmounting is safe.
|
|
167
|
+
*
|
|
168
|
+
* Lifecycle per row:
|
|
169
|
+
* - a row that newly maps to an activity → `shell.upsertActivity(...)` (creates
|
|
170
|
+
* it, stamping `createdAt`);
|
|
171
|
+
* - an owned row whose mapping changes → `shell.updateActivity(id, patch)`
|
|
172
|
+
* (preserves `createdAt`, bumps `updatedAt`, and the shell emits a
|
|
173
|
+
* `transition` event when `status` changed — driving toasts);
|
|
174
|
+
* - a row that vanishes, or whose mapping flips to `null` →
|
|
175
|
+
* `shell.removeActivity(id)`.
|
|
176
|
+
*
|
|
177
|
+
* Only activities this feed created are ever touched: activities pushed to the
|
|
178
|
+
* shell by other sources (or a second feed) are left untouched, and `dispose`
|
|
179
|
+
* removes exactly this feed's set.
|
|
180
|
+
*
|
|
181
|
+
* @typeParam TData - the row DTO shape carried by {@link ActivityFeedOptions.collection}.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```svelte
|
|
185
|
+
* <script lang="ts">
|
|
186
|
+
* import { activityFeed } from '@happyvertical/smrt-svelte/web';
|
|
187
|
+
* import { useAdminShell } from '@happyvertical/smrt-svelte/workspace';
|
|
188
|
+
* import { createSmrtCollection } from '@happyvertical/smrt-web';
|
|
189
|
+
* import { getCollectionDefinition } from '@happyvertical/smrt-virt-web';
|
|
190
|
+
*
|
|
191
|
+
* const shell = useAdminShell();
|
|
192
|
+
* const encodes = createSmrtCollection(getCollectionDefinition('encodes'), {});
|
|
193
|
+
*
|
|
194
|
+
* // Reconciles `encodes` rows into the shell's Focus scope; disposes on unmount.
|
|
195
|
+
* activityFeed({
|
|
196
|
+
* collection: encodes,
|
|
197
|
+
* shell,
|
|
198
|
+
* map: (row) => ({
|
|
199
|
+
* kind: 'video-encode',
|
|
200
|
+
* scope: 'focus',
|
|
201
|
+
* label: row.title,
|
|
202
|
+
* status: row.state, // 'running' | 'completed' | …
|
|
203
|
+
* progress: row.progress,
|
|
204
|
+
* }),
|
|
205
|
+
* });
|
|
206
|
+
* </script>
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export declare function activityFeed<TData extends object>(options: ActivityFeedOptions<TData>): ActivityFeedHandle;
|
|
210
|
+
//# sourceMappingURL=activity-feed.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activity-feed.svelte.d.ts","sourceRoot":"","sources":["../../src/web/activity-feed.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qDAAqD,CAAC;AACtF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8CAA8C,CAAC;AAGlF;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACnC,aAAa,EACb,IAAI,GAAG,WAAW,GAAG,WAAW,CACjC,GAAG;IACF;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,eAAe,CAAC,KAAK,SAAS,MAAM,IAAI,CAClD,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,KACnB,kBAAkB,GAAG,IAAI,CAAC;AAE/B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB,CAAC,KAAK,SAAS,MAAM;IACvD;;;;OAIG;IACH,UAAU,EAAE,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACrC;;;OAGG;IACH,GAAG,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC;IAC5B,qEAAqE;IACrE,KAAK,EAAE,UAAU,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;OAMG;IACH,OAAO,IAAI,IAAI,CAAC;IAChB,yCAAyC;IACzC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AA+GD;;;;;;;;;;GAUG;AACH,qBAAa,sBAAsB,CAAC,KAAK,SAAS,MAAM;IAUpD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAVtB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoC;IAC1D,OAAO,CAAC,QAAQ,CAAS;gBAGN,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,eAAe,CAAC,KAAK,CAAC;IAG9C,yCAAyC;IACzC,IAAI,UAAU,IAAI,OAAO,CAExB;IAED;;;;;OAKG;IACH,SAAS,CAAC,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI;IA+CvD;;;OAGG;IACH,OAAO,IAAI,IAAI;CAMhB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,wBAAgB,YAAY,CAAC,KAAK,SAAS,MAAM,EAC/C,OAAO,EAAE,mBAAmB,CAAC,KAAK,CAAC,GAClC,kBAAkB,CAyCpB"}
|