@checkstack/notification-backend 0.1.23 → 0.2.1
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/CHANGELOG.md +104 -0
- package/package.json +11 -9
- package/src/cache.ts +101 -0
- package/src/index.ts +7 -1
- package/src/router.ts +46 -26
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,109 @@
|
|
|
1
1
|
# @checkstack/notification-backend
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [208ad71]
|
|
8
|
+
- @checkstack/signal-common@0.2.0
|
|
9
|
+
- @checkstack/notification-common@0.3.0
|
|
10
|
+
- @checkstack/backend-api@0.13.1
|
|
11
|
+
- @checkstack/auth-backend@0.4.21
|
|
12
|
+
- @checkstack/cache-api@0.2.1
|
|
13
|
+
- @checkstack/queue-api@0.2.15
|
|
14
|
+
- @checkstack/cache-utils@0.2.1
|
|
15
|
+
|
|
16
|
+
## 0.2.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 8d1ef12: ## Per-entity caching with single-flight + safe invalidation across the dashboard hot paths
|
|
21
|
+
|
|
22
|
+
### `@checkstack/cache-api`
|
|
23
|
+
|
|
24
|
+
- **Breaking** for backend implementors: `CacheProvider` now requires `deleteByPrefix(prefix: string): Promise<number>` for family-level invalidation. The in-memory provider implements it; downstream providers (Redis, etc.) must add it before upgrading.
|
|
25
|
+
- `createScopedCache` forwards `deleteByPrefix` and keeps prefixes scoped to the calling plugin.
|
|
26
|
+
|
|
27
|
+
### `@checkstack/cache-utils` (new package)
|
|
28
|
+
|
|
29
|
+
High-level read-through caching helpers built on `CacheProvider`:
|
|
30
|
+
|
|
31
|
+
- `createCachedScope({ cacheManager, pluginId })` returns a scope with `wrap`, `wrapMany`, `invalidate`, and `invalidatePrefix`.
|
|
32
|
+
- **Single-flight**: concurrent cache misses for the same key share one loader.
|
|
33
|
+
- **Per-entity bulk caching** via `wrapMany` so list/bulk RPCs cache by id rather than by the full input shape — overlapping callers share entries and invalidation stays exact.
|
|
34
|
+
- **Race-safe invalidation** via per-key epoch counters: a loader started before a mutation cannot repopulate the cache with stale data after the mutation invalidates it. The mutation invariant is `db.write → cache.invalidate (await) → signals.emit`.
|
|
35
|
+
- Cache failures fall through to the loader so a cache outage cannot break reads.
|
|
36
|
+
|
|
37
|
+
### `@checkstack/backend`
|
|
38
|
+
|
|
39
|
+
- The internal null `CacheProvider` (used when no cache backend is configured) now implements the new `deleteByPrefix` method as a no-op. Patch bump only — no behavior change for existing callers.
|
|
40
|
+
|
|
41
|
+
### `@checkstack/healthcheck-backend`
|
|
42
|
+
|
|
43
|
+
- `getSystemHealthStatus` and `getBulkSystemHealthStatus` now read through a per-system cache (`healthcheck:status:<systemId>`), eliminating N database queries per dashboard refresh for unchanged systems.
|
|
44
|
+
- Mutation paths (configuration CRUD, system associations, satellite ingest, queue-driven check runs, system/satellite removal hooks) invalidate affected keys before broadcasting their signals so frontend refetches always observe fresh data.
|
|
45
|
+
|
|
46
|
+
### `@checkstack/incident-backend`
|
|
47
|
+
|
|
48
|
+
- `listIncidents`, `getIncident`, `getIncidentsForSystem`, and `getBulkIncidentsForSystems` now read through a scoped cache:
|
|
49
|
+
- per-incident at `incident:<id>`
|
|
50
|
+
- per-system at `system:<systemId>`
|
|
51
|
+
- per-filter-shape at `list:<stable-stringify(filters)>` for the few list shapes the dashboard polls
|
|
52
|
+
- Mutations (`createIncident`, `updateIncident`, `addUpdate`, `resolveIncident`, `deleteIncident`) invalidate the incident, every affected system, and every cached list before broadcasting `INCIDENT_UPDATED`.
|
|
53
|
+
- The catalog `systemDeleted` cleanup hook drops that system's cached entries.
|
|
54
|
+
|
|
55
|
+
### `@checkstack/maintenance-backend`
|
|
56
|
+
|
|
57
|
+
- `listMaintenances`, `getMaintenance`, `getMaintenancesForSystem`, and `getBulkMaintenancesForSystems` use the same per-entity / per-system / per-filter-shape pattern as incidents.
|
|
58
|
+
- Mutations (`createMaintenance`, `updateMaintenance`, `addUpdate`, `closeMaintenance`, `deleteMaintenance`) invalidate before broadcasting `MAINTENANCE_UPDATED`.
|
|
59
|
+
|
|
60
|
+
### `@checkstack/catalog-backend`
|
|
61
|
+
|
|
62
|
+
- Topology reads (`getEntities`, `getSystems`, `getSystem`, `getGroups`, `getSystemGroupIds`) cache under the `entity:` family (25s TTL).
|
|
63
|
+
- Views (`getViews`) and per-system contacts (`getSystemContacts`) cache in their own families.
|
|
64
|
+
- System / group / membership mutations drop the entire `entity:` family (every reader joins the same tables); view and contact mutations drop only their respective scopes.
|
|
65
|
+
|
|
66
|
+
### `@checkstack/slo-backend`
|
|
67
|
+
|
|
68
|
+
- `listObjectives`, `getObjective`, `getObjectivesForSystem`, and `getBulkObjectivesForSystems` cache results including the expensive `engine.computeStatus` output.
|
|
69
|
+
- Per-entity caching for the bulk handler so dashboards with overlapping system sets share entries.
|
|
70
|
+
- Mutations (`createObjective`, `updateObjective`, `deleteObjective`) invalidate before broadcasting `SLO_STATUS_CHANGED`.
|
|
71
|
+
|
|
72
|
+
### `@checkstack/anomaly-backend`
|
|
73
|
+
|
|
74
|
+
- New `router-cache.ts` adds a cache scope distinct from the existing detector baseline cache, keyed by stable filter hash.
|
|
75
|
+
- `getAnomalies` and `getAnomalyBaselines` cache through this scope (15s TTL).
|
|
76
|
+
- The detector invalidates the router cache before broadcasting `ANOMALY_STATE_CHANGED` on every state transition (suspicious/anomaly/recovered).
|
|
77
|
+
- Config mutations also invalidate.
|
|
78
|
+
|
|
79
|
+
### `@checkstack/notification-backend`
|
|
80
|
+
|
|
81
|
+
- `getUnreadCount`, `getNotifications`, and `getSubscriptions` cache per-user.
|
|
82
|
+
- `markAsRead`, `deleteNotification`, `notifyUsers`, and `notifyGroups` invalidate every affected user's cache before sending realtime signals to that user.
|
|
83
|
+
- `subscribe` and `unsubscribe` invalidate the user's subscription cache.
|
|
84
|
+
|
|
85
|
+
### `@checkstack/announcement-backend`
|
|
86
|
+
|
|
87
|
+
- `getActiveAnnouncements` caches per-user (or anonymous) and per-`includeDismissed` flag (45s TTL — admin-driven, slowly changing).
|
|
88
|
+
- `listAllAnnouncements` caches under a single key.
|
|
89
|
+
- `dismissAnnouncement` only drops that user's cache; `createAnnouncement`, `updateAnnouncement`, `deleteAnnouncement` drop every user's cache before broadcasting `ANNOUNCEMENT_UPDATED`.
|
|
90
|
+
- The auth `userDeleted` cleanup hook drops that user's cached entries.
|
|
91
|
+
|
|
92
|
+
### Patch Changes
|
|
93
|
+
|
|
94
|
+
- Updated dependencies [8d1ef12]
|
|
95
|
+
- Updated dependencies [8d1ef12]
|
|
96
|
+
- Updated dependencies [8d1ef12]
|
|
97
|
+
- @checkstack/common@0.7.0
|
|
98
|
+
- @checkstack/cache-api@0.2.0
|
|
99
|
+
- @checkstack/cache-utils@0.2.0
|
|
100
|
+
- @checkstack/backend-api@0.13.0
|
|
101
|
+
- @checkstack/auth-backend@0.4.20
|
|
102
|
+
- @checkstack/auth-common@0.6.3
|
|
103
|
+
- @checkstack/notification-common@0.2.9
|
|
104
|
+
- @checkstack/signal-common@0.1.10
|
|
105
|
+
- @checkstack/queue-api@0.2.14
|
|
106
|
+
|
|
3
107
|
## 0.1.23
|
|
4
108
|
|
|
5
109
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/notification-backend",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"checkstack": {
|
|
@@ -14,22 +14,24 @@
|
|
|
14
14
|
"test": "bun test"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@checkstack/notification-common": "0.2.
|
|
18
|
-
"@checkstack/backend-api": "0.
|
|
19
|
-
"@checkstack/
|
|
20
|
-
"@checkstack/
|
|
21
|
-
"@checkstack/
|
|
22
|
-
"@checkstack/
|
|
17
|
+
"@checkstack/notification-common": "0.2.9",
|
|
18
|
+
"@checkstack/backend-api": "0.13.0",
|
|
19
|
+
"@checkstack/cache-api": "0.2.0",
|
|
20
|
+
"@checkstack/cache-utils": "0.2.0",
|
|
21
|
+
"@checkstack/signal-common": "0.1.10",
|
|
22
|
+
"@checkstack/queue-api": "0.2.14",
|
|
23
|
+
"@checkstack/auth-backend": "0.4.20",
|
|
24
|
+
"@checkstack/auth-common": "0.6.3",
|
|
23
25
|
"drizzle-orm": "^0.45.0",
|
|
24
26
|
"zod": "^4.2.1",
|
|
25
|
-
"@checkstack/common": "0.
|
|
27
|
+
"@checkstack/common": "0.7.0",
|
|
26
28
|
"@orpc/server": "^1.13.2"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
31
|
"@checkstack/drizzle-helper": "0.0.4",
|
|
30
32
|
"@checkstack/scripts": "0.1.2",
|
|
31
33
|
"@checkstack/tsconfig": "0.0.5",
|
|
32
|
-
"@checkstack/test-utils-backend": "0.1.
|
|
34
|
+
"@checkstack/test-utils-backend": "0.1.20",
|
|
33
35
|
"@types/node": "^20.0.0",
|
|
34
36
|
"typescript": "^5.0.0"
|
|
35
37
|
}
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { CacheManager } from "@checkstack/cache-api";
|
|
2
|
+
import {
|
|
3
|
+
createCachedScope,
|
|
4
|
+
type CachedScope,
|
|
5
|
+
} from "@checkstack/cache-utils";
|
|
6
|
+
import type { Logger } from "@checkstack/backend-api";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 15s. The NotificationBell on every page polls `getUnreadCount` every 30s,
|
|
10
|
+
* so even short caching provides huge savings. Mutations send signals to the
|
|
11
|
+
* affected users which trigger frontend refetches — invalidation must
|
|
12
|
+
* complete before the signal so the refetch hits a fresh DB read.
|
|
13
|
+
*/
|
|
14
|
+
const NOTIFICATION_TTL_MS = 15_000;
|
|
15
|
+
|
|
16
|
+
const UNREAD_PREFIX = "unread:";
|
|
17
|
+
const NOTIFS_PREFIX = "notifs:";
|
|
18
|
+
const SUBS_PREFIX = "subs:";
|
|
19
|
+
|
|
20
|
+
const unreadKey = (userId: string): string => `${UNREAD_PREFIX}${userId}`;
|
|
21
|
+
const subsKey = (userId: string): string => `${SUBS_PREFIX}${userId}`;
|
|
22
|
+
|
|
23
|
+
function stableStringify(value: unknown): string {
|
|
24
|
+
if (value === null || typeof value !== "object") {
|
|
25
|
+
return JSON.stringify(value);
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return `[${value.map((v) => stableStringify(v)).join(",")}]`;
|
|
29
|
+
}
|
|
30
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
31
|
+
.filter(([, v]) => v !== undefined)
|
|
32
|
+
.toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
33
|
+
return `{${entries
|
|
34
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`)
|
|
35
|
+
.join(",")}}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const notifsKey = (userId: string, filters: unknown): string =>
|
|
39
|
+
`${NOTIFS_PREFIX}${userId}:${stableStringify(filters ?? {})}`;
|
|
40
|
+
const notifsPrefixForUser = (userId: string): string =>
|
|
41
|
+
`${NOTIFS_PREFIX}${userId}:`;
|
|
42
|
+
|
|
43
|
+
export interface NotificationCache {
|
|
44
|
+
wrapUnread: <T>(userId: string, loader: () => Promise<T>) => Promise<T>;
|
|
45
|
+
wrapNotifications: <T>(
|
|
46
|
+
userId: string,
|
|
47
|
+
filters: unknown,
|
|
48
|
+
loader: () => Promise<T>,
|
|
49
|
+
) => Promise<T>;
|
|
50
|
+
wrapSubscriptions: <T>(
|
|
51
|
+
userId: string,
|
|
52
|
+
loader: () => Promise<T>,
|
|
53
|
+
) => Promise<T>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Drop unread + notifications cache for a single user. Used after a
|
|
57
|
+
* mutation that adds/removes/marks-as-read a notification for that user.
|
|
58
|
+
*/
|
|
59
|
+
invalidateForUser: (userId: string) => Promise<void>;
|
|
60
|
+
|
|
61
|
+
/** Drop subscriptions cache for one user (subscribe/unsubscribe). */
|
|
62
|
+
invalidateSubscriptions: (userId: string) => Promise<void>;
|
|
63
|
+
|
|
64
|
+
scope: CachedScope;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createNotificationCache({
|
|
68
|
+
cacheManager,
|
|
69
|
+
logger,
|
|
70
|
+
}: {
|
|
71
|
+
cacheManager: CacheManager;
|
|
72
|
+
logger: Logger;
|
|
73
|
+
}): NotificationCache {
|
|
74
|
+
const scope = createCachedScope({
|
|
75
|
+
cacheManager,
|
|
76
|
+
pluginId: "notification",
|
|
77
|
+
defaultTtlMs: NOTIFICATION_TTL_MS,
|
|
78
|
+
onError: (op: string, error: unknown) => {
|
|
79
|
+
logger.warn(`notification cache ${op} failed: ${String(error)}`);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
wrapUnread: (userId, loader) => scope.wrap(unreadKey(userId), loader),
|
|
85
|
+
wrapNotifications: (userId, filters, loader) =>
|
|
86
|
+
scope.wrap(notifsKey(userId, filters), loader),
|
|
87
|
+
wrapSubscriptions: (userId, loader) =>
|
|
88
|
+
scope.wrap(subsKey(userId), loader),
|
|
89
|
+
|
|
90
|
+
invalidateForUser: async (userId) => {
|
|
91
|
+
await Promise.all([
|
|
92
|
+
scope.invalidate(unreadKey(userId)),
|
|
93
|
+
scope.invalidatePrefix(notifsPrefixForUser(userId)),
|
|
94
|
+
]);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
invalidateSubscriptions: (userId) => scope.invalidate(subsKey(userId)),
|
|
98
|
+
|
|
99
|
+
scope,
|
|
100
|
+
};
|
|
101
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { eq } from "drizzle-orm";
|
|
|
21
21
|
|
|
22
22
|
import * as schema from "./schema";
|
|
23
23
|
import { createNotificationRouter } from "./router";
|
|
24
|
+
import { createNotificationCache } from "./cache";
|
|
24
25
|
import { authHooks } from "@checkstack/auth-backend";
|
|
25
26
|
import { createOAuthCallbackHandler } from "./oauth-callback-handler";
|
|
26
27
|
import { createStrategyService } from "./strategy-service";
|
|
@@ -157,6 +158,7 @@ export default createBackendPlugin({
|
|
|
157
158
|
rpcClient: coreServices.rpcClient,
|
|
158
159
|
config: coreServices.config,
|
|
159
160
|
signalService: coreServices.signalService,
|
|
161
|
+
cacheManager: coreServices.cacheManager,
|
|
160
162
|
},
|
|
161
163
|
init: async ({
|
|
162
164
|
logger,
|
|
@@ -165,6 +167,7 @@ export default createBackendPlugin({
|
|
|
165
167
|
rpcClient,
|
|
166
168
|
config,
|
|
167
169
|
signalService,
|
|
170
|
+
cacheManager,
|
|
168
171
|
}) => {
|
|
169
172
|
logger.debug("🔔 Initializing Notification Backend...");
|
|
170
173
|
|
|
@@ -184,6 +187,8 @@ export default createBackendPlugin({
|
|
|
184
187
|
env as unknown as { strategyService: typeof strategyService }
|
|
185
188
|
).strategyService = strategyService;
|
|
186
189
|
|
|
190
|
+
const cache = createNotificationCache({ cacheManager, logger });
|
|
191
|
+
|
|
187
192
|
// Create and register the notification router with strategy registry
|
|
188
193
|
const router = createNotificationRouter(
|
|
189
194
|
db,
|
|
@@ -191,7 +196,8 @@ export default createBackendPlugin({
|
|
|
191
196
|
signalService,
|
|
192
197
|
strategyRegistry,
|
|
193
198
|
rpcClient,
|
|
194
|
-
logger
|
|
199
|
+
logger,
|
|
200
|
+
cache,
|
|
195
201
|
);
|
|
196
202
|
rpc.registerRouter(router, notificationContract);
|
|
197
203
|
|
package/src/router.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "@checkstack/notification-common";
|
|
19
19
|
import { AuthApi } from "@checkstack/auth-common";
|
|
20
20
|
import type { SignalService } from "@checkstack/signal-common";
|
|
21
|
+
import type { NotificationCache } from "./cache";
|
|
21
22
|
import { SafeDatabase } from "@checkstack/backend-api";
|
|
22
23
|
import * as schema from "./schema";
|
|
23
24
|
import {
|
|
@@ -95,7 +96,8 @@ export const createNotificationRouter = (
|
|
|
95
96
|
signalService: SignalService,
|
|
96
97
|
strategyRegistry: NotificationStrategyRegistry,
|
|
97
98
|
rpcApi: RpcClient,
|
|
98
|
-
logger: Logger
|
|
99
|
+
logger: Logger,
|
|
100
|
+
cache: NotificationCache,
|
|
99
101
|
) => {
|
|
100
102
|
// Create strategy service for config management
|
|
101
103
|
const strategyService: StrategyService = createStrategyService({
|
|
@@ -269,39 +271,46 @@ export const createNotificationRouter = (
|
|
|
269
271
|
// context.user is guaranteed to be RealUser by contract meta + autoAuthMiddleware
|
|
270
272
|
const userId = (context.user as RealUser).id;
|
|
271
273
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
274
|
+
return cache.wrapNotifications(userId, input, async () => {
|
|
275
|
+
const result = await getUserNotifications(database, userId, {
|
|
276
|
+
limit: input.limit,
|
|
277
|
+
offset: input.offset,
|
|
278
|
+
unreadOnly: input.unreadOnly,
|
|
279
|
+
});
|
|
277
280
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
281
|
+
return {
|
|
282
|
+
notifications: result.notifications.map((n) => ({
|
|
283
|
+
id: n.id,
|
|
284
|
+
userId: n.userId,
|
|
285
|
+
title: n.title,
|
|
286
|
+
body: n.body,
|
|
287
|
+
action: n.action ?? undefined,
|
|
288
|
+
importance: n.importance as "info" | "warning" | "critical",
|
|
289
|
+
isRead: n.isRead,
|
|
290
|
+
groupId: n.groupId ?? undefined,
|
|
291
|
+
createdAt: n.createdAt,
|
|
292
|
+
})),
|
|
293
|
+
total: result.total,
|
|
294
|
+
};
|
|
295
|
+
});
|
|
292
296
|
}
|
|
293
297
|
),
|
|
294
298
|
|
|
295
299
|
getUnreadCount: os.getUnreadCount.handler(async ({ context }) => {
|
|
296
300
|
const userId = (context.user as RealUser).id;
|
|
297
|
-
|
|
298
|
-
|
|
301
|
+
return cache.wrapUnread(userId, async () => {
|
|
302
|
+
const count = await getUnreadCount(database, userId);
|
|
303
|
+
return { count };
|
|
304
|
+
});
|
|
299
305
|
}),
|
|
300
306
|
|
|
301
307
|
markAsRead: os.markAsRead.handler(async ({ input, context }) => {
|
|
302
308
|
const userId = (context.user as RealUser).id;
|
|
303
309
|
await markAsRead(database, userId, input.notificationId);
|
|
304
310
|
|
|
311
|
+
// Mutation invariant: db.write → cache.invalidate (await) → signal.
|
|
312
|
+
await cache.invalidateForUser(userId);
|
|
313
|
+
|
|
305
314
|
// Send signal to update NotificationBell in realtime
|
|
306
315
|
void signalService.sendToUser(NOTIFICATION_READ, userId, {
|
|
307
316
|
notificationId: input.notificationId,
|
|
@@ -312,6 +321,7 @@ export const createNotificationRouter = (
|
|
|
312
321
|
async ({ input, context }) => {
|
|
313
322
|
const userId = (context.user as RealUser).id;
|
|
314
323
|
await deleteNotification(database, userId, input.notificationId);
|
|
324
|
+
await cache.invalidateForUser(userId);
|
|
315
325
|
}
|
|
316
326
|
),
|
|
317
327
|
|
|
@@ -333,11 +343,9 @@ export const createNotificationRouter = (
|
|
|
333
343
|
|
|
334
344
|
getSubscriptions: os.getSubscriptions.handler(async ({ context }) => {
|
|
335
345
|
const userId = (context.user as RealUser).id;
|
|
336
|
-
|
|
337
|
-
database,
|
|
338
|
-
userId
|
|
346
|
+
return cache.wrapSubscriptions(userId, () =>
|
|
347
|
+
getEnrichedUserSubscriptions(database, userId),
|
|
339
348
|
);
|
|
340
|
-
return subscriptions;
|
|
341
349
|
}),
|
|
342
350
|
|
|
343
351
|
subscribe: os.subscribe.handler(async ({ input, context }) => {
|
|
@@ -354,11 +362,13 @@ export const createNotificationRouter = (
|
|
|
354
362
|
}
|
|
355
363
|
throw error;
|
|
356
364
|
}
|
|
365
|
+
await cache.invalidateSubscriptions(userId);
|
|
357
366
|
}),
|
|
358
367
|
|
|
359
368
|
unsubscribe: os.unsubscribe.handler(async ({ input, context }) => {
|
|
360
369
|
const userId = (context.user as RealUser).id;
|
|
361
370
|
await unsubscribeFromGroup(database, userId, input.groupId);
|
|
371
|
+
await cache.invalidateSubscriptions(userId);
|
|
362
372
|
}),
|
|
363
373
|
|
|
364
374
|
// ==========================================================================
|
|
@@ -465,6 +475,12 @@ export const createNotificationRouter = (
|
|
|
465
475
|
userId: schema.notifications.userId,
|
|
466
476
|
});
|
|
467
477
|
|
|
478
|
+
// Drop each affected user's cache before signaling, so the
|
|
479
|
+
// NotificationBell's refetch sees the new unread count.
|
|
480
|
+
await Promise.all(
|
|
481
|
+
userIds.map((userId) => cache.invalidateForUser(userId)),
|
|
482
|
+
);
|
|
483
|
+
|
|
468
484
|
// Send realtime signals to each user
|
|
469
485
|
for (const notification of inserted) {
|
|
470
486
|
void signalService.sendToUser(
|
|
@@ -527,6 +543,10 @@ export const createNotificationRouter = (
|
|
|
527
543
|
userId: schema.notifications.userId,
|
|
528
544
|
});
|
|
529
545
|
|
|
546
|
+
await Promise.all(
|
|
547
|
+
subscribers.map((sub) => cache.invalidateForUser(sub.userId)),
|
|
548
|
+
);
|
|
549
|
+
|
|
530
550
|
// Send realtime signals to each subscriber
|
|
531
551
|
for (const notification of inserted) {
|
|
532
552
|
void signalService.sendToUser(
|