@ttt-productions/notification-core 0.15.17 → 0.15.18

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.
Files changed (87) hide show
  1. package/README.md +9 -10
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +0 -3
  4. package/dist/index.js.map +1 -1
  5. package/dist/react/components/NotificationEmptyState.d.ts +0 -3
  6. package/dist/react/components/NotificationEmptyState.d.ts.map +1 -1
  7. package/dist/react/components/NotificationEmptyState.js +0 -3
  8. package/dist/react/components/NotificationEmptyState.js.map +1 -1
  9. package/dist/react/components/NotificationHistoryList.d.ts +0 -6
  10. package/dist/react/components/NotificationHistoryList.d.ts.map +1 -1
  11. package/dist/react/components/NotificationHistoryList.js +0 -8
  12. package/dist/react/components/NotificationHistoryList.js.map +1 -1
  13. package/dist/react/components/NotificationList.d.ts +0 -3
  14. package/dist/react/components/NotificationList.d.ts.map +1 -1
  15. package/dist/react/components/NotificationList.js +2 -29
  16. package/dist/react/components/NotificationList.js.map +1 -1
  17. package/dist/react/components/NotificationUnreadBadge.d.ts +0 -5
  18. package/dist/react/components/NotificationUnreadBadge.d.ts.map +1 -1
  19. package/dist/react/components/NotificationUnreadBadge.js +0 -5
  20. package/dist/react/components/NotificationUnreadBadge.js.map +1 -1
  21. package/dist/react/components/relative-time.d.ts +0 -3
  22. package/dist/react/components/relative-time.d.ts.map +1 -1
  23. package/dist/react/components/relative-time.js +0 -3
  24. package/dist/react/components/relative-time.js.map +1 -1
  25. package/dist/react/hooks/useActiveNotifications.d.ts +0 -4
  26. package/dist/react/hooks/useActiveNotifications.d.ts.map +1 -1
  27. package/dist/react/hooks/useActiveNotifications.js +2 -6
  28. package/dist/react/hooks/useActiveNotifications.js.map +1 -1
  29. package/dist/react/hooks/useArchiveAllNotifications.d.ts +0 -30
  30. package/dist/react/hooks/useArchiveAllNotifications.d.ts.map +1 -1
  31. package/dist/react/hooks/useArchiveAllNotifications.js +1 -43
  32. package/dist/react/hooks/useArchiveAllNotifications.js.map +1 -1
  33. package/dist/react/hooks/useArchiveNotification.d.ts +0 -8
  34. package/dist/react/hooks/useArchiveNotification.d.ts.map +1 -1
  35. package/dist/react/hooks/useArchiveNotification.js +0 -8
  36. package/dist/react/hooks/useArchiveNotification.js.map +1 -1
  37. package/dist/react/hooks/useNotificationHistory.d.ts +0 -14
  38. package/dist/react/hooks/useNotificationHistory.d.ts.map +1 -1
  39. package/dist/react/hooks/useNotificationHistory.js +2 -18
  40. package/dist/react/hooks/useNotificationHistory.js.map +1 -1
  41. package/dist/react/hooks/useUnreadCount.d.ts +0 -22
  42. package/dist/react/hooks/useUnreadCount.d.ts.map +1 -1
  43. package/dist/react/hooks/useUnreadCount.js +1 -22
  44. package/dist/react/hooks/useUnreadCount.js.map +1 -1
  45. package/dist/react/index.d.ts.map +1 -1
  46. package/dist/react/index.js +0 -2
  47. package/dist/react/index.js.map +1 -1
  48. package/dist/react/types.d.ts +0 -42
  49. package/dist/react/types.d.ts.map +1 -1
  50. package/dist/react/types.js.map +1 -1
  51. package/dist/schemas.d.ts +0 -22
  52. package/dist/schemas.d.ts.map +1 -1
  53. package/dist/schemas.js +0 -25
  54. package/dist/schemas.js.map +1 -1
  55. package/dist/server/activeNotificationId.d.ts +1 -22
  56. package/dist/server/activeNotificationId.d.ts.map +1 -1
  57. package/dist/server/activeNotificationId.js +0 -16
  58. package/dist/server/activeNotificationId.js.map +1 -1
  59. package/dist/server/delivery-ledger.d.ts +1 -73
  60. package/dist/server/delivery-ledger.d.ts.map +1 -1
  61. package/dist/server/delivery-ledger.js +7 -59
  62. package/dist/server/delivery-ledger.js.map +1 -1
  63. package/dist/server/errors.d.ts +0 -9
  64. package/dist/server/errors.d.ts.map +1 -1
  65. package/dist/server/errors.js +1 -11
  66. package/dist/server/errors.js.map +1 -1
  67. package/dist/server/index.d.ts.map +1 -1
  68. package/dist/server/index.js +0 -3
  69. package/dist/server/index.js.map +1 -1
  70. package/dist/server/observed-generation.d.ts +1 -41
  71. package/dist/server/observed-generation.d.ts.map +1 -1
  72. package/dist/server/observed-generation.js +1 -31
  73. package/dist/server/observed-generation.js.map +1 -1
  74. package/dist/server/processBatchHelper.d.ts +0 -19
  75. package/dist/server/processBatchHelper.d.ts.map +1 -1
  76. package/dist/server/processBatchHelper.js +2 -46
  77. package/dist/server/processBatchHelper.js.map +1 -1
  78. package/dist/server/types.d.ts +0 -9
  79. package/dist/server/types.d.ts.map +1 -1
  80. package/dist/server/types.js +0 -4
  81. package/dist/server/types.js.map +1 -1
  82. package/dist/types.d.ts +2 -196
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js +0 -6
  85. package/dist/types.js.map +1 -1
  86. package/package.json +5 -5
  87. package/src/styles/notifications.css +0 -14
package/dist/types.d.ts CHANGED
@@ -1,150 +1,56 @@
1
- /**
2
- * @ttt-productions/notification-core — Type definitions
3
- *
4
- * Two-tier notification system: active (unread) → history (archived).
5
- * No `isRead` flag — existence in the active collection means unread.
6
- */
7
- /**
8
- * Active notification document stored in Firestore.
9
- * Same shape for both user and admin collections.
10
- */
11
1
  export interface NotificationDoc {
12
- /** Firestore doc ID */
13
2
  id: string;
14
- /** Notification type (e.g. 'content_report', 'entity_invite') */
15
3
  type: string;
16
- /** Dedup key (e.g. 'report_entityXYZ') — same key = same notification */
17
4
  dedupKey: string;
18
- /** Category: 'user' | 'admin' (extensible) */
19
5
  category: string;
20
- /** userId for 'user' category, null for 'admin' (shared) */
21
6
  targetUserId: string | null;
22
- /** Notification title */
23
7
  title: string;
24
- /** Notification body */
25
8
  message: string;
26
- /** Incremented on duplicates */
27
9
  count: number;
28
- /**
29
- * Last N actor user IDs (capped). Identity is stored id-only — display names
30
- * are resolved at read time by the consuming app (e.g. from publicUsers), so
31
- * they never drift in the persisted doc.
32
- */
33
10
  latestActorIds: string[];
34
- /**
35
- * Route path when clicked (e.g. '/admin' or '/entities/abc'). ABSENT when the
36
- * type declares no `defaultTargetPath` — a linkless, informational-only
37
- * notification (the consumer renders no "go to" affordance for it).
38
- */
39
11
  targetPath?: string;
40
- /** Type-specific metadata (e.g. { entityId, reason }) */
41
12
  metadata: Record<string, unknown>;
42
- /**
43
- * Epoch ms the recipient last marked this notification seen, or `0` when
44
- * unseen. Generic field — no domain knowledge. Always present (initialized
45
- * to `0` on creation and reset to `0` on dedup-increment) so the unread
46
- * `count()` aggregation can match it with a `seenAt == 0` equality predicate.
47
- * `seenAt` is a personal concept; shared notifications carry it for shape
48
- * uniformity but their unread indicator is existence-based and ignores it.
49
- */
13
+ /** `0` when unseen and never absent: a `count()` equality predicate cannot match a missing field. */
50
14
  seenAt: number;
51
- /**
52
- * Opaque per-generation token (uuid), minted FRESH whenever the active doc is
53
- * created or materially re-lit. The SEEN/ARCHIVE precondition compares against
54
- * THIS, not a restartable integer: the active doc id is deterministic, so a
55
- * delete+recreate would restart an integer and let a stale tab's "version 1"
56
- * match a different card it never saw (ABA). An opaque token cannot repeat.
57
- * Optional on the legacy active doc shape; the ledger materializer always sets it.
58
- */
59
15
  activityGeneration?: string;
60
- /** The `activityGeneration` observed at the time `seenAt` was last set. */
61
16
  seenAtGeneration?: string;
62
- /** First occurrence */
63
17
  createdAt: number;
64
- /** Latest occurrence */
65
18
  updatedAt: number;
66
19
  }
67
- /**
68
- * Pending notification — queue item for the batch processor.
69
- */
70
20
  export interface PendingNotification {
71
21
  id: string;
72
- /** Notification type */
73
22
  type: string;
74
- /** Target category */
75
23
  category: string;
76
- /** Target userId (null for admin/shared) */
77
24
  targetUserId: string | null;
78
- /** Who triggered this (id-only; display name resolved at read time) */
79
25
  actorId: string;
80
- /** Type-specific data for building the notification */
81
26
  metadata: Record<string, unknown>;
82
- /** Epoch ms */
83
27
  createdAt: number;
84
28
  }
85
- /**
86
- * Per-category configuration (user vs admin).
87
- */
88
29
  export interface NotificationCategoryConfig {
89
- /** Firestore collection path for active notifications */
90
30
  activePath: string;
91
- /** Function that returns the history collection path */
92
31
  historyPath: (userId?: string) => string;
93
- /** 'personal' requires targetUserId, 'shared' = all members see everything */
94
32
  audienceType: 'personal' | 'shared';
95
33
  }
96
- /**
97
- * Per-type configuration for a notification type.
98
- */
99
34
  export interface NotificationTypeConfig {
100
- /** Which category this type belongs to */
101
35
  category: string;
102
- /** Delivery mode */
103
36
  delivery: 'realtime' | 'queued';
104
- /** Function to build the dedupKey from metadata */
105
37
  dedupKeyPattern: (metadata: Record<string, unknown>) => string;
106
- /** Function to build the title from metadata */
107
38
  titlePattern: (metadata: Record<string, unknown>) => string;
108
- /** Function to build the message from metadata + count */
109
39
  messagePattern: (metadata: Record<string, unknown>, count: number) => string;
110
- /**
111
- * Default target path, or function to build from metadata. OMIT to declare a
112
- * linkless type: the written doc carries no `targetPath` and the consumer
113
- * renders no "go to" affordance (clear-only rows).
114
- */
115
40
  defaultTargetPath?: string | ((metadata: Record<string, unknown>) => string);
116
- /** Max count value (default 5000) */
117
41
  countCap?: number;
118
- /** Max latestActorIds array length (default 5) */
119
42
  actorCap?: number;
120
- /** Icon/emoji for display (optional) */
121
43
  icon?: string;
122
44
  }
123
- /**
124
- * Full notification system configuration — provided by each app.
125
- */
126
45
  export interface NotificationSystemConfig {
127
- /** Category definitions */
128
46
  categories: Record<string, NotificationCategoryConfig>;
129
- /** Type definitions */
130
47
  types: Record<string, NotificationTypeConfig>;
131
- /** Batch processor interval in minutes (default 10) */
132
48
  batchIntervalMinutes?: number;
133
- /** Firestore collection for pending notifications (default 'pendingNotifications') */
134
49
  pendingCollectionPath?: string;
135
- /** Firestore collection for the delivery ledger (default 'notificationDeliveries'). */
136
50
  deliveriesCollectionPath?: string;
137
- /**
138
- * Factory that converts epoch-ms to the app's Firestore `Timestamp` instance.
139
- * REQUIRED for the delivery ledger: Firestore native TTL acts ONLY on
140
- * `Timestamp` fields, so `expireAt` must be a real Timestamp (never a number).
141
- * The generic package never imports firebase-admin, so the app injects this
142
- * (e.g. `(ms) => admin.firestore.Timestamp.fromMillis(ms)`).
143
- */
51
+ /** Required by the ledger: Firestore TTL acts only on `Timestamp` fields, never a number. */
144
52
  timestampFromMillis?: (ms: number) => unknown;
145
- /** Delivery-row TTL after materialization, in ms (default 90 days). */
146
53
  deliveryTtlMs?: number;
147
- /** Max delivery attempts before a row dead-letters (default 8). */
148
54
  maxDeliveryAttempts?: number;
149
55
  }
150
56
  export interface UseActiveNotificationsOptions {
@@ -153,22 +59,11 @@ export interface UseActiveNotificationsOptions {
153
59
  category: string;
154
60
  enabled?: boolean;
155
61
  pageSize?: number;
156
- /** Re-read the displayed page every `refetchInterval` ms while mounted (default 30s). */
157
62
  refetchInterval?: number;
158
- /** How long a read page counts as fresh, in ms (default 30s). */
159
63
  staleTime?: number;
160
64
  }
161
- /**
162
- * A flattened archived-history item for the read surface. The history doc is a
163
- * wrapper (`archivedSnapshot` + archive metadata + TTL); this shape lifts the
164
- * snapshot's display fields to the top level so the history list renders exactly
165
- * like the active list, plus `archivedAt` for ordering/display and
166
- * `archiveOccurrenceId` as the stable React key.
167
- */
168
65
  export interface NotificationHistoryItem extends NotificationDoc {
169
- /** The history doc id (stable key — the `archiveOccurrenceId`). */
170
66
  archiveOccurrenceId: string;
171
- /** Epoch ms the notification was archived (history docs order by this). */
172
67
  archivedAt: number;
173
68
  }
174
69
  export interface UseNotificationHistoryOptions {
@@ -177,7 +72,6 @@ export interface UseNotificationHistoryOptions {
177
72
  category: string;
178
73
  enabled?: boolean;
179
74
  pageSize?: number;
180
- /** Read-freshness stale time in ms (archived rows are immutable; default 60s). */
181
75
  staleTime?: number;
182
76
  }
183
77
  export interface UseUnreadCountOptions {
@@ -188,85 +82,31 @@ export interface UseUnreadCountOptions {
188
82
  refetchInterval?: number;
189
83
  countLimit?: number;
190
84
  }
191
- /**
192
- * What an archive adapter answers: whether the card was archived. `false` means the
193
- * server archived nothing (for example the card gained new activity after it was
194
- * rendered); the row stays active and is not shown as clearing.
195
- */
196
85
  export interface NotificationArchiveResult {
197
86
  archived: boolean;
198
87
  }
199
- /**
200
- * App-supplied adapter that archives one rendered notification (active → history).
201
- * It receives the row exactly as the list rendered it, so the app can send what it
202
- * observed (e.g. the row's activity generation). It rejects on failure and resolves
203
- * whether the card was archived.
204
- */
205
88
  export type NotificationArchiveFn = (notification: NotificationDoc) => Promise<NotificationArchiveResult>;
206
89
  export interface UseArchiveNotificationOptions {
207
90
  userId: string;
208
91
  category: string;
209
- /**
210
- * Performs the archive through the app's callable. The hook performs no client
211
- * Firestore writes; it only invalidates the read keys on success.
212
- */
213
92
  archiveFn: NotificationArchiveFn;
214
93
  invalidateKeys?: readonly unknown[][];
215
94
  }
216
- /**
217
- * Terminal + transient states of a server-owned archive-all job, as surfaced to the UI.
218
- *
219
- * - `idle` — nothing enqueued yet (initial / reset state).
220
- * - `enqueuing` — the enqueue adapter is in flight (job not yet acknowledged).
221
- * - `in-progress` — the job is enqueued/running; the worker is still draining the category.
222
- * - `complete` — TERMINAL: the worker fully drained the category's active set.
223
- * - `incomplete` — TERMINAL: the job stopped without draining everything (e.g. hit its own
224
- * bound / a non-advanceable card). The tray keeps a "some remain — try again"
225
- * affordance, mirroring the old loop's incomplete result.
226
- * - `failed` — TERMINAL: the enqueue call threw, or the job dead-lettered / reported failure.
227
- */
228
95
  export type ArchiveAllJobStatus = 'idle' | 'enqueuing' | 'in-progress' | 'complete' | 'incomplete' | 'failed';
229
- /**
230
- * Result of the INJECTED enqueue adapter — triggers exactly one server-owned archive-all job for
231
- * the given category and returns immediately with the job's id (idempotent per user+category+request
232
- * on the app side). The app wires this to its `httpsCallable(functions, 'enqueueArchiveAll')`
233
- * (or equivalent); the generic package never names the callable.
234
- */
235
96
  export interface EnqueueArchiveAllResult {
236
- /** Server job id the poller then polls via the status adapter. */
237
97
  jobId: string;
238
98
  }
239
- /**
240
- * Snapshot of a server-owned archive-all job, returned by the INJECTED status-query adapter.
241
- * The poller maps `state` → {@link ArchiveAllJobStatus} and stops polling on any terminal state.
242
- * `category` echoes the tab/category the job was enqueued for so the UI can assert per-tab scoping.
243
- */
244
99
  export interface ArchiveAllJobSnapshot {
245
- /** Job lifecycle state as owned by the server worker. */
246
100
  state: 'in-progress' | 'complete' | 'incomplete' | 'failed';
247
- /** The notification category/tab this job is scoped to (must match the enqueue category). */
248
101
  category: string;
249
- /** Cards archived so far (monotonic); optional progress for the UI. */
250
102
  archived?: number;
251
- /**
252
- * On a `complete` job: cards the drain could not archive, which are still in the list. A job
253
- * completes once it has walked the whole category, so a card that keeps failing on its own does
254
- * not hold the rest of the category back.
255
- */
256
103
  notArchived?: number;
257
- /** Human-readable failure reason when `state === 'failed'` (optional). */
258
104
  error?: string;
259
105
  }
260
- /**
261
- * Aggregate result the poller resolves with once the job reaches a terminal state.
262
- * `complete` is the load-bearing completion contract (true ONLY when the whole category was
263
- * drained), preserved from the old loop so callers gate side effects the same way.
264
- */
265
106
  export interface ArchiveAllPollResult {
266
107
  status: Extract<ArchiveAllJobStatus, 'complete' | 'incomplete' | 'failed'>;
267
108
  category: string;
268
109
  archived: number;
269
- /** Cards a complete drain left in the list (each failed on its own); 0 otherwise. */
270
110
  notArchived: number;
271
111
  complete: boolean;
272
112
  error?: string;
@@ -274,50 +114,16 @@ export interface ArchiveAllPollResult {
274
114
  export interface UseArchiveAllNotificationsOptions {
275
115
  userId: string;
276
116
  category: string;
277
- /**
278
- * App-supplied adapter that ENQUEUES one server-owned archive-all job scoped to `category` and
279
- * returns its `jobId`. The `category` is passed so the app callable can carry the tab/source into
280
- * the job (per-tab scoping is REQUIRED — the worker archives only that category's active cards).
281
- * Typically `(category) => enqueueArchiveAll({ category })` over an `httpsCallable`.
282
- */
283
117
  enqueueArchiveAllFn: (category: string) => Promise<EnqueueArchiveAllResult>;
284
- /**
285
- * App-supplied adapter that returns the current {@link ArchiveAllJobSnapshot} for a job id — the
286
- * hook polls it until the job reaches a terminal state. Typically
287
- * `(jobId) => getArchiveAllStatus({ jobId })` over an `httpsCallable`, or a Firestore read of the
288
- * job doc. No client Firestore writes.
289
- */
290
118
  getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;
291
- /** Poll interval in ms while the job is in-progress (default 1500). */
292
119
  pollIntervalMs?: number;
293
- /**
294
- * Max number of status polls before the poller gives up and resolves `incomplete` (guards a job
295
- * that never reaches a terminal state — default 120, i.e. ~3 min at the default interval).
296
- */
297
120
  maxPolls?: number;
298
121
  invalidateKeys?: readonly unknown[][];
299
122
  }
300
- /**
301
- * Per-row actions the list hands to `renderRowAction`. The row itself is inert
302
- * — every affordance lives in the controls the consumer
303
- * renders in the slot. `archive` performs the active→history archive for that
304
- * row; it is present ONLY on the active list (archived history rows cannot be
305
- * re-archived, so it is absent there).
306
- */
307
123
  export interface NotificationRowActions {
308
- /** Archives this row; rejects when the archive fails. */
309
124
  archive?: () => Promise<void>;
310
- /**
311
- * True while this row is archiving, including a category-wide Clear All. After an
312
- * archive the server confirmed, it stays true until the row leaves the active list.
313
- */
314
125
  isArchivePending?: boolean;
315
126
  }
316
- /**
317
- * What a list hands its `renderError` slot when its read failed. `hasRows` is true when
318
- * rows from an earlier read are still on screen above the slot (a failed refresh);
319
- * `retry` re-reads the displayed page and `retrying` is that read's pending flag.
320
- */
321
127
  export interface NotificationListErrorState {
322
128
  error: Error;
323
129
  retry: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js CHANGED
@@ -1,8 +1,2 @@
1
- /**
2
- * @ttt-productions/notification-core — Type definitions
3
- *
4
- * Two-tier notification system: active (unread) → history (archived).
5
- * No `isRead` flag — existence in the active collection means unread.
6
- */
7
1
  export {};
8
2
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * @ttt-productions/notification-core — Type definitions\n *\n * Two-tier notification system: active (unread) → history (archived).\n * No `isRead` flag — existence in the active collection means unread.\n */\n\n// ============================================================================\n// DOCUMENT TYPES\n// ============================================================================\n\n/**\n * Active notification document stored in Firestore.\n * Same shape for both user and admin collections.\n */\nexport interface NotificationDoc {\n /** Firestore doc ID */\n id: string;\n /** Notification type (e.g. 'content_report', 'entity_invite') */\n type: string;\n /** Dedup key (e.g. 'report_entityXYZ') — same key = same notification */\n dedupKey: string;\n /** Category: 'user' | 'admin' (extensible) */\n category: string;\n\n // Targeting\n /** userId for 'user' category, null for 'admin' (shared) */\n targetUserId: string | null;\n\n // Display\n /** Notification title */\n title: string;\n /** Notification body */\n message: string;\n\n // Dedup aggregation\n /** Incremented on duplicates */\n count: number;\n /**\n * Last N actor user IDs (capped). Identity is stored id-only — display names\n * are resolved at read time by the consuming app (e.g. from publicUsers), so\n * they never drift in the persisted doc.\n */\n latestActorIds: string[];\n\n // Navigation\n /**\n * Route path when clicked (e.g. '/admin' or '/entities/abc'). ABSENT when the\n * type declares no `defaultTargetPath` — a linkless, informational-only\n * notification (the consumer renders no \"go to\" affordance for it).\n */\n targetPath?: string;\n /** Type-specific metadata (e.g. { entityId, reason }) */\n metadata: Record<string, unknown>;\n\n // Seen state (personal notifications)\n /**\n * Epoch ms the recipient last marked this notification seen, or `0` when\n * unseen. Generic field — no domain knowledge. Always present (initialized\n * to `0` on creation and reset to `0` on dedup-increment) so the unread\n * `count()` aggregation can match it with a `seenAt == 0` equality predicate.\n * `seenAt` is a personal concept; shared notifications carry it for shape\n * uniformity but their unread indicator is existence-based and ignores it.\n */\n seenAt: number;\n\n /**\n * Opaque per-generation token (uuid), minted FRESH whenever the active doc is\n * created or materially re-lit. The SEEN/ARCHIVE precondition compares against\n * THIS, not a restartable integer: the active doc id is deterministic, so a\n * delete+recreate would restart an integer and let a stale tab's \"version 1\"\n * match a different card it never saw (ABA). An opaque token cannot repeat.\n * Optional on the legacy active doc shape; the ledger materializer always sets it.\n */\n activityGeneration?: string;\n /** The `activityGeneration` observed at the time `seenAt` was last set. */\n seenAtGeneration?: string;\n\n // Timestamps (epoch ms)\n /** First occurrence */\n createdAt: number;\n /** Latest occurrence */\n updatedAt: number;\n}\n\n// Archived history docs are persisted as an untyped wrapper by the server archive\n// helper (server/observed-generation.ts) — `archivedSnapshot` (the full active doc)\n// plus archive metadata + a native-TTL `expireAt` Timestamp. There is no consumer-\n// facing TS type for that shape; the canonical registry schema lives in the\n// consuming app's core package (doc-schemas/notifications.ts → NotificationHistoryDocSchema).\n\n/**\n * Pending notification — queue item for the batch processor.\n */\nexport interface PendingNotification {\n id: string;\n /** Notification type */\n type: string;\n /** Target category */\n category: string;\n /** Target userId (null for admin/shared) */\n targetUserId: string | null;\n /** Who triggered this (id-only; display name resolved at read time) */\n actorId: string;\n /** Type-specific data for building the notification */\n metadata: Record<string, unknown>;\n /** Epoch ms */\n createdAt: number;\n}\n\n// ============================================================================\n// CONFIGURATION TYPES\n// ============================================================================\n\n/**\n * Per-category configuration (user vs admin).\n */\nexport interface NotificationCategoryConfig {\n /** Firestore collection path for active notifications */\n activePath: string;\n /** Function that returns the history collection path */\n historyPath: (userId?: string) => string;\n /** 'personal' requires targetUserId, 'shared' = all members see everything */\n audienceType: 'personal' | 'shared';\n}\n\n/**\n * Per-type configuration for a notification type.\n */\nexport interface NotificationTypeConfig {\n /** Which category this type belongs to */\n category: string;\n /** Delivery mode */\n delivery: 'realtime' | 'queued';\n /** Function to build the dedupKey from metadata */\n dedupKeyPattern: (metadata: Record<string, unknown>) => string;\n /** Function to build the title from metadata */\n titlePattern: (metadata: Record<string, unknown>) => string;\n /** Function to build the message from metadata + count */\n messagePattern: (metadata: Record<string, unknown>, count: number) => string;\n /**\n * Default target path, or function to build from metadata. OMIT to declare a\n * linkless type: the written doc carries no `targetPath` and the consumer\n * renders no \"go to\" affordance (clear-only rows).\n */\n defaultTargetPath?: string | ((metadata: Record<string, unknown>) => string);\n /** Max count value (default 5000) */\n countCap?: number;\n /** Max latestActorIds array length (default 5) */\n actorCap?: number;\n /** Icon/emoji for display (optional) */\n icon?: string;\n}\n\n/**\n * Full notification system configuration — provided by each app.\n */\nexport interface NotificationSystemConfig {\n /** Category definitions */\n categories: Record<string, NotificationCategoryConfig>;\n /** Type definitions */\n types: Record<string, NotificationTypeConfig>;\n /** Batch processor interval in minutes (default 10) */\n batchIntervalMinutes?: number;\n /** Firestore collection for pending notifications (default 'pendingNotifications') */\n pendingCollectionPath?: string;\n /** Firestore collection for the delivery ledger (default 'notificationDeliveries'). */\n deliveriesCollectionPath?: string;\n /**\n * Factory that converts epoch-ms to the app's Firestore `Timestamp` instance.\n * REQUIRED for the delivery ledger: Firestore native TTL acts ONLY on\n * `Timestamp` fields, so `expireAt` must be a real Timestamp (never a number).\n * The generic package never imports firebase-admin, so the app injects this\n * (e.g. `(ms) => admin.firestore.Timestamp.fromMillis(ms)`).\n */\n timestampFromMillis?: (ms: number) => unknown;\n /** Delivery-row TTL after materialization, in ms (default 90 days). */\n deliveryTtlMs?: number;\n /** Max delivery attempts before a row dead-letters (default 8). */\n maxDeliveryAttempts?: number;\n}\n\n// ============================================================================\n// HOOK OPTION TYPES\n// ============================================================================\n\nexport interface UseActiveNotificationsOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n pageSize?: number;\n /** Re-read the displayed page every `refetchInterval` ms while mounted (default 30s). */\n refetchInterval?: number;\n /** How long a read page counts as fresh, in ms (default 30s). */\n staleTime?: number;\n}\n\n/**\n * A flattened archived-history item for the read surface. The history doc is a\n * wrapper (`archivedSnapshot` + archive metadata + TTL); this shape lifts the\n * snapshot's display fields to the top level so the history list renders exactly\n * like the active list, plus `archivedAt` for ordering/display and\n * `archiveOccurrenceId` as the stable React key.\n */\nexport interface NotificationHistoryItem extends NotificationDoc {\n /** The history doc id (stable key — the `archiveOccurrenceId`). */\n archiveOccurrenceId: string;\n /** Epoch ms the notification was archived (history docs order by this). */\n archivedAt: number;\n}\n\nexport interface UseNotificationHistoryOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n pageSize?: number;\n /** Read-freshness stale time in ms (archived rows are immutable; default 60s). */\n staleTime?: number;\n}\n\nexport interface UseUnreadCountOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n refetchInterval?: number;\n countLimit?: number;\n}\n\n/**\n * What an archive adapter answers: whether the card was archived. `false` means the\n * server archived nothing (for example the card gained new activity after it was\n * rendered); the row stays active and is not shown as clearing.\n */\nexport interface NotificationArchiveResult {\n archived: boolean;\n}\n\n/**\n * App-supplied adapter that archives one rendered notification (active → history).\n * It receives the row exactly as the list rendered it, so the app can send what it\n * observed (e.g. the row's activity generation). It rejects on failure and resolves\n * whether the card was archived.\n */\nexport type NotificationArchiveFn = (\n notification: NotificationDoc,\n) => Promise<NotificationArchiveResult>;\n\nexport interface UseArchiveNotificationOptions {\n userId: string;\n category: string;\n /**\n * Performs the archive through the app's callable. The hook performs no client\n * Firestore writes; it only invalidates the read keys on success.\n */\n archiveFn: NotificationArchiveFn;\n invalidateKeys?: readonly unknown[][];\n}\n\n// ----------------------------------------------------------------------------\n// Archive-all — server-owned job + thin client status poller\n// ----------------------------------------------------------------------------\n//\n// Archive-all is NOT a browser loop. The client enqueues ONE server-owned\n// archive-all job (scoped to a single notification category/tab), then polls its\n// status until the job reaches a terminal state. All paging + ownership-checked\n// per-card deletion happen server-side in a bounded scheduled/queued worker; the\n// hook never touches Firestore and never chains archive calls. The generic\n// package hardcodes NO callable name — the app injects both adapters below.\n\n/**\n * Terminal + transient states of a server-owned archive-all job, as surfaced to the UI.\n *\n * - `idle` — nothing enqueued yet (initial / reset state).\n * - `enqueuing` — the enqueue adapter is in flight (job not yet acknowledged).\n * - `in-progress` — the job is enqueued/running; the worker is still draining the category.\n * - `complete` — TERMINAL: the worker fully drained the category's active set.\n * - `incomplete` — TERMINAL: the job stopped without draining everything (e.g. hit its own\n * bound / a non-advanceable card). The tray keeps a \"some remain — try again\"\n * affordance, mirroring the old loop's incomplete result.\n * - `failed` — TERMINAL: the enqueue call threw, or the job dead-lettered / reported failure.\n */\nexport type ArchiveAllJobStatus =\n 'idle' | 'enqueuing' | 'in-progress' | 'complete' | 'incomplete' | 'failed';\n\n/**\n * Result of the INJECTED enqueue adapter — triggers exactly one server-owned archive-all job for\n * the given category and returns immediately with the job's id (idempotent per user+category+request\n * on the app side). The app wires this to its `httpsCallable(functions, 'enqueueArchiveAll')`\n * (or equivalent); the generic package never names the callable.\n */\nexport interface EnqueueArchiveAllResult {\n /** Server job id the poller then polls via the status adapter. */\n jobId: string;\n}\n\n/**\n * Snapshot of a server-owned archive-all job, returned by the INJECTED status-query adapter.\n * The poller maps `state` → {@link ArchiveAllJobStatus} and stops polling on any terminal state.\n * `category` echoes the tab/category the job was enqueued for so the UI can assert per-tab scoping.\n */\nexport interface ArchiveAllJobSnapshot {\n /** Job lifecycle state as owned by the server worker. */\n state: 'in-progress' | 'complete' | 'incomplete' | 'failed';\n /** The notification category/tab this job is scoped to (must match the enqueue category). */\n category: string;\n /** Cards archived so far (monotonic); optional progress for the UI. */\n archived?: number;\n /**\n * On a `complete` job: cards the drain could not archive, which are still in the list. A job\n * completes once it has walked the whole category, so a card that keeps failing on its own does\n * not hold the rest of the category back.\n */\n notArchived?: number;\n /** Human-readable failure reason when `state === 'failed'` (optional). */\n error?: string;\n}\n\n/**\n * Aggregate result the poller resolves with once the job reaches a terminal state.\n * `complete` is the load-bearing completion contract (true ONLY when the whole category was\n * drained), preserved from the old loop so callers gate side effects the same way.\n */\nexport interface ArchiveAllPollResult {\n status: Extract<ArchiveAllJobStatus, 'complete' | 'incomplete' | 'failed'>;\n category: string;\n archived: number;\n /** Cards a complete drain left in the list (each failed on its own); 0 otherwise. */\n notArchived: number;\n complete: boolean;\n error?: string;\n}\n\nexport interface UseArchiveAllNotificationsOptions {\n userId: string;\n category: string;\n /**\n * App-supplied adapter that ENQUEUES one server-owned archive-all job scoped to `category` and\n * returns its `jobId`. The `category` is passed so the app callable can carry the tab/source into\n * the job (per-tab scoping is REQUIRED — the worker archives only that category's active cards).\n * Typically `(category) => enqueueArchiveAll({ category })` over an `httpsCallable`.\n */\n enqueueArchiveAllFn: (category: string) => Promise<EnqueueArchiveAllResult>;\n /**\n * App-supplied adapter that returns the current {@link ArchiveAllJobSnapshot} for a job id — the\n * hook polls it until the job reaches a terminal state. Typically\n * `(jobId) => getArchiveAllStatus({ jobId })` over an `httpsCallable`, or a Firestore read of the\n * job doc. No client Firestore writes.\n */\n getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;\n /** Poll interval in ms while the job is in-progress (default 1500). */\n pollIntervalMs?: number;\n /**\n * Max number of status polls before the poller gives up and resolves `incomplete` (guards a job\n * that never reaches a terminal state — default 120, i.e. ~3 min at the default interval).\n */\n maxPolls?: number;\n invalidateKeys?: readonly unknown[][];\n}\n\n// ============================================================================\n// COMPONENT PROP TYPES\n// ============================================================================\n\n/**\n * Per-row actions the list hands to `renderRowAction`. The row itself is inert\n * — every affordance lives in the controls the consumer\n * renders in the slot. `archive` performs the active→history archive for that\n * row; it is present ONLY on the active list (archived history rows cannot be\n * re-archived, so it is absent there).\n */\nexport interface NotificationRowActions {\n /** Archives this row; rejects when the archive fails. */\n archive?: () => Promise<void>;\n /**\n * True while this row is archiving, including a category-wide Clear All. After an\n * archive the server confirmed, it stays true until the row leaves the active list.\n */\n isArchivePending?: boolean;\n}\n\n/**\n * What a list hands its `renderError` slot when its read failed. `hasRows` is true when\n * rows from an earlier read are still on screen above the slot (a failed refresh);\n * `retry` re-reads the displayed page and `retrying` is that read's pending flag.\n */\nexport interface NotificationListErrorState {\n error: Error;\n retry: () => void;\n retrying: boolean;\n hasRows: boolean;\n}\n\nexport interface NotificationEmptyStateProps {\n text?: string;\n}\n\nexport interface NotificationUnreadBadgeProps {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n refetchInterval?: number;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export interface NotificationDoc {\n id: string;\n type: string;\n dedupKey: string;\n category: string;\n\n targetUserId: string | null;\n\n title: string;\n message: string;\n\n count: number;\n latestActorIds: string[];\n\n targetPath?: string;\n metadata: Record<string, unknown>;\n\n /** `0` when unseen and never absent: a `count()` equality predicate cannot match a missing field. */\n seenAt: number;\n\n activityGeneration?: string;\n seenAtGeneration?: string;\n\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface PendingNotification {\n id: string;\n type: string;\n category: string;\n targetUserId: string | null;\n actorId: string;\n metadata: Record<string, unknown>;\n createdAt: number;\n}\n\nexport interface NotificationCategoryConfig {\n activePath: string;\n historyPath: (userId?: string) => string;\n audienceType: 'personal' | 'shared';\n}\n\nexport interface NotificationTypeConfig {\n category: string;\n delivery: 'realtime' | 'queued';\n dedupKeyPattern: (metadata: Record<string, unknown>) => string;\n titlePattern: (metadata: Record<string, unknown>) => string;\n messagePattern: (metadata: Record<string, unknown>, count: number) => string;\n defaultTargetPath?: string | ((metadata: Record<string, unknown>) => string);\n countCap?: number;\n actorCap?: number;\n icon?: string;\n}\n\nexport interface NotificationSystemConfig {\n categories: Record<string, NotificationCategoryConfig>;\n types: Record<string, NotificationTypeConfig>;\n batchIntervalMinutes?: number;\n pendingCollectionPath?: string;\n deliveriesCollectionPath?: string;\n /** Required by the ledger: Firestore TTL acts only on `Timestamp` fields, never a number. */\n timestampFromMillis?: (ms: number) => unknown;\n deliveryTtlMs?: number;\n maxDeliveryAttempts?: number;\n}\n\nexport interface UseActiveNotificationsOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n pageSize?: number;\n refetchInterval?: number;\n staleTime?: number;\n}\n\nexport interface NotificationHistoryItem extends NotificationDoc {\n archiveOccurrenceId: string;\n archivedAt: number;\n}\n\nexport interface UseNotificationHistoryOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n pageSize?: number;\n staleTime?: number;\n}\n\nexport interface UseUnreadCountOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n refetchInterval?: number;\n countLimit?: number;\n}\n\nexport interface NotificationArchiveResult {\n archived: boolean;\n}\n\nexport type NotificationArchiveFn = (\n notification: NotificationDoc,\n) => Promise<NotificationArchiveResult>;\n\nexport interface UseArchiveNotificationOptions {\n userId: string;\n category: string;\n archiveFn: NotificationArchiveFn;\n invalidateKeys?: readonly unknown[][];\n}\n\nexport type ArchiveAllJobStatus =\n 'idle' | 'enqueuing' | 'in-progress' | 'complete' | 'incomplete' | 'failed';\n\nexport interface EnqueueArchiveAllResult {\n jobId: string;\n}\n\nexport interface ArchiveAllJobSnapshot {\n state: 'in-progress' | 'complete' | 'incomplete' | 'failed';\n category: string;\n archived?: number;\n notArchived?: number;\n error?: string;\n}\n\nexport interface ArchiveAllPollResult {\n status: Extract<ArchiveAllJobStatus, 'complete' | 'incomplete' | 'failed'>;\n category: string;\n archived: number;\n notArchived: number;\n complete: boolean;\n error?: string;\n}\n\nexport interface UseArchiveAllNotificationsOptions {\n userId: string;\n category: string;\n enqueueArchiveAllFn: (category: string) => Promise<EnqueueArchiveAllResult>;\n getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;\n pollIntervalMs?: number;\n maxPolls?: number;\n invalidateKeys?: readonly unknown[][];\n}\n\nexport interface NotificationRowActions {\n archive?: () => Promise<void>;\n isArchivePending?: boolean;\n}\n\nexport interface NotificationListErrorState {\n error: Error;\n retry: () => void;\n retrying: boolean;\n hasRows: boolean;\n}\n\nexport interface NotificationEmptyStateProps {\n text?: string;\n}\n\nexport interface NotificationUnreadBadgeProps {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n refetchInterval?: number;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/notification-core",
3
- "version": "0.15.17",
3
+ "version": "0.15.18",
4
4
  "description": "Shared notification system for TTT Productions apps — active/history two-tier architecture with dedup, batch processing, and themed UI components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,8 +42,8 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@tanstack/react-query": ">=5.0.0",
45
- "@ttt-productions/query-core": "^0.14.22",
46
- "@ttt-productions/ui-core": "^0.11.34",
45
+ "@ttt-productions/query-core": "^0.14.23",
46
+ "@ttt-productions/ui-core": "^0.11.35",
47
47
  "firebase": ">=12.0.0",
48
48
  "react": ">=19.0.0",
49
49
  "react-dom": ">=19.0.0"
@@ -70,8 +70,8 @@
70
70
  },
71
71
  "devDependencies": {
72
72
  "@tanstack/react-query": "^5.0.0",
73
- "@ttt-productions/query-core": "^0.14.22",
74
- "@ttt-productions/ui-core": "^0.11.34",
73
+ "@ttt-productions/query-core": "^0.14.23",
74
+ "@ttt-productions/ui-core": "^0.11.35",
75
75
  "@types/react": "^19.0.0",
76
76
  "@types/react-dom": "^19.0.0",
77
77
  "firebase": "^12.14.0",
@@ -1,10 +1,3 @@
1
- /* ============================================================================
2
- * @ttt-productions/notification-core — Notification Styles
3
- *
4
- * Uses theme-core CSS variables for automatic light/dark/high-contrast support.
5
- * All classes use the `ntf-` prefix to avoid collisions.
6
- * Apps can override any class for custom branding.
7
- * ============================================================================ */
8
1
 
9
2
  /* === NOTIFICATION LIST === */
10
3
  .ntf-list {
@@ -13,7 +6,6 @@
13
6
  max-height: 24rem;
14
7
  }
15
8
 
16
- /* Header stays pinned; only the body scrolls. */
17
9
  .ntf-list-body {
18
10
  flex: 1;
19
11
  min-height: 0;
@@ -62,9 +54,6 @@
62
54
  }
63
55
 
64
56
  /* === NOTIFICATION ITEM === */
65
- /* The row itself is inert — every affordance is a control in its action slot — so it wears no
66
- * pointer cursor. Its hover tint helps the eye along a long list; it moves on theme-core's motion
67
- * tokens, so an app's reduced-motion kill switch covers it. */
68
57
  .ntf-item {
69
58
  display: flex;
70
59
  align-items: flex-start;
@@ -127,9 +116,6 @@
127
116
  align-self: center;
128
117
  }
129
118
 
130
- /* Sibling action slot next to the archive control — consumer-supplied via
131
- * `renderRowAction`. Purely a layout/isolation wrapper; the consumer's node
132
- * supplies its own visual treatment. */
133
119
  .ntf-item-row-action {
134
120
  flex-shrink: 0;
135
121
  align-self: center;