mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-04 21:15:50 +00:00
feat(notifications): node-local notif.written hook
NotificationService emits notif.written after each non-silent write and after an in-place rewrite, on the node that wrote the row, so an extension can act on new notifications without a second bus round trip. It stays off the published event stream; notif.created carries the notification itself.
This commit is contained in:
1 parent
47a6122c37
commit
fceaee6372
4 files changed
+112
-1
No files matched your search
@@ -552,6 +552,17 @@ export type EventMap = {
|
||||
/** Events handler runs behind whatever created it. */
|
||||
handlerDepth?: number;
|
||||
};
|
||||
/** Every non-silent write or update, only on the node that wrote it. */
|
||||
'notif.written': {
|
||||
userId: number;
|
||||
uid: string;
|
||||
type: string;
|
||||
/** The row's app. Absent on an update, which keeps the row's own. */
|
||||
appUid?: string | null;
|
||||
value: Record<string, unknown>;
|
||||
/** An earlier notification rewritten in place. */
|
||||
updated?: true;
|
||||
};
|
||||
/**
|
||||
* One notification, addressed at whatever sockets each region holds for the
|
||||
* recipient. `outer.*` reaches peer regions and never sibling nodes, so
|
||||
|
||||
@@ -444,6 +444,10 @@ export const UNPUBLISHED_INTERNAL_EVENTS = [
|
||||
event: 'kv.share.retired',
|
||||
reason: 'A share handle stopping is not a change to any key; it ends the subscriptions made through the handle rather than being delivered to them.',
|
||||
},
|
||||
{
|
||||
event: 'notif.written',
|
||||
reason: 'A node-local hook after a notification is written, for extensions; the notification itself publishes through `notif.created`.',
|
||||
},
|
||||
] as const satisfies readonly UnpublishedInternalEvent[];
|
||||
|
||||
// Every `fs.*`/`kv.*`/`notif.*` bus key needs a decision. A new key that is in
|
||||
|
||||
@@ -338,6 +338,79 @@ describe('NotificationService.notifyUpdate', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("NotificationService — 'notif.written'", () => {
|
||||
it('fires once per recipient whose row was written', async () => {
|
||||
const a = await makeUser();
|
||||
const b = await makeUser();
|
||||
const appUid = `app-${uuidv4()}`;
|
||||
const written = collect('notif.written');
|
||||
|
||||
await notifications.notify(
|
||||
[a.id, b.id],
|
||||
{ title: 'ended' },
|
||||
{ type: 'app.events.ended', appUid },
|
||||
);
|
||||
await waitFor(() => written.seen.length === 2);
|
||||
|
||||
const rows = await Promise.all(
|
||||
[a, b].map((u) =>
|
||||
server.stores.notification.listByUserId(u.id, {}),
|
||||
),
|
||||
);
|
||||
const expected = [a, b].map((u, i) => ({
|
||||
userId: u.id,
|
||||
uid: rows[i][0].uid,
|
||||
type: 'app.events.ended',
|
||||
appUid,
|
||||
value: { title: 'ended', type: 'app.events.ended' },
|
||||
}));
|
||||
expect(written.seen).toEqual(expect.arrayContaining(expected));
|
||||
written.stop();
|
||||
});
|
||||
|
||||
it('stays quiet for a silent write', async () => {
|
||||
const user = await makeUser();
|
||||
const written = collect('notif.written');
|
||||
|
||||
await notifications.notify(
|
||||
[user.id],
|
||||
{ title: 'quiet' },
|
||||
{ type: 'share.received', silent: true },
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
|
||||
expect(written.seen).toEqual([]);
|
||||
written.stop();
|
||||
});
|
||||
|
||||
it('marks a rewrite in place as updated', async () => {
|
||||
const user = await makeUser();
|
||||
const uid = await notifications.notify(
|
||||
[user.id],
|
||||
{ title: 'one share' },
|
||||
{ type: 'share.received' },
|
||||
);
|
||||
const written = collect('notif.written');
|
||||
|
||||
await notifications.notifyUpdate(
|
||||
uid!,
|
||||
user.id,
|
||||
{ title: 'two shares' },
|
||||
{ type: 'share.received' },
|
||||
);
|
||||
await waitFor(() => written.seen.length === 1);
|
||||
|
||||
expect(written.seen[0]).toEqual({
|
||||
userId: user.id,
|
||||
uid,
|
||||
type: 'share.received',
|
||||
value: { title: 'two shares', type: 'share.received' },
|
||||
updated: true,
|
||||
});
|
||||
written.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe('NotificationService.markAcknowledged', () => {
|
||||
it("acknowledges the row and pushes an ack to the user's other tabs", async () => {
|
||||
const user = await makeUser();
|
||||
|
||||
@@ -169,12 +169,24 @@ export class NotificationService extends PuterService {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!opts.silent)
|
||||
if (!opts.silent) {
|
||||
this.clients.event.emit(
|
||||
'notif.written',
|
||||
{
|
||||
userId,
|
||||
uid,
|
||||
type: registered.type,
|
||||
appUid,
|
||||
value: payload,
|
||||
},
|
||||
{},
|
||||
);
|
||||
await this.#push(userId, uid, payload, {
|
||||
type: registered.type,
|
||||
audience: registered.audience,
|
||||
appUid,
|
||||
});
|
||||
}
|
||||
return uid;
|
||||
}),
|
||||
);
|
||||
@@ -217,6 +229,17 @@ export class NotificationService extends PuterService {
|
||||
// to anything deduplicating on `event.id`.
|
||||
this.#socket().message(userId, uid, payload);
|
||||
this.#markDelivered(uid, userId);
|
||||
this.clients.event.emit(
|
||||
'notif.written',
|
||||
{
|
||||
userId,
|
||||
uid,
|
||||
type: registered.type,
|
||||
value: payload,
|
||||
updated: true,
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user