@voltro/plugin-notifications 0.84.0 → 0.86.0

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/dist/rpc.d.ts CHANGED
@@ -5,10 +5,10 @@ import { ReactivityChannel } from '@voltro/protocol';
5
5
  import { Schema } from 'effect';
6
6
 
7
7
  export declare const archiveDescriptor: MutationProcedureDescriptor<"notifications.archive", Schema.Struct<{
8
- id: typeof Schema.String;
8
+ readonly id: Schema.String;
9
9
  }>, Schema.Struct<{
10
- ok: typeof Schema.Boolean;
11
- }>, typeof Schema.Never>;
10
+ readonly ok: Schema.Boolean;
11
+ }>, Schema.Never>;
12
12
 
13
13
  /**
14
14
  * The plugin's canonical package name — the key an app's `alias` is recorded
@@ -23,104 +23,104 @@ export declare const archiveDescriptor: MutationProcedureDescriptor<"notificatio
23
23
  export declare const BASE_NAME = "@voltro/plugin-notifications";
24
24
 
25
25
  export declare const clearQuietHoursDescriptor: MutationProcedureDescriptor<"notifications.clearQuietHours", Schema.Struct<{}>, Schema.Struct<{
26
- ok: typeof Schema.Boolean;
27
- }>, typeof Schema.Never>;
26
+ readonly ok: Schema.Boolean;
27
+ }>, Schema.Never>;
28
28
 
29
29
  export declare const inboxDescriptor: QueryProcedureDescriptor<"notifications.inbox", Schema.Struct<{
30
- unreadOnly: Schema.optional<typeof Schema.Boolean>;
31
- limit: Schema.optional<typeof Schema.Number>;
32
- }>, Schema.Array$<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>, typeof Schema.Never>;
30
+ readonly unreadOnly: Schema.optional<Schema.Boolean>;
31
+ readonly limit: Schema.optional<Schema.Number>;
32
+ }>, Schema.$Array<Schema.$Record<Schema.String, Schema.Unknown>>, Schema.Never>;
33
33
 
34
34
  /** One push channel for the inbox: every in-app delivery and read-state
35
35
  * change publishes, and `inbox` / `unreadCount` re-run for subscribers. */
36
36
  export declare const inboxFeed: ReactivityChannel;
37
37
 
38
38
  export declare const markAllReadDescriptor: MutationProcedureDescriptor<"notifications.markAllRead", Schema.Struct<{}>, Schema.Struct<{
39
- count: typeof Schema.Number;
40
- }>, typeof Schema.Never>;
39
+ readonly count: Schema.Number;
40
+ }>, Schema.Never>;
41
41
 
42
42
  export declare const markReadDescriptor: MutationProcedureDescriptor<"notifications.markRead", Schema.Struct<{
43
- id: typeof Schema.String;
43
+ readonly id: Schema.String;
44
44
  }>, Schema.Struct<{
45
- ok: typeof Schema.Boolean;
46
- }>, typeof Schema.Never>;
45
+ readonly ok: Schema.Boolean;
46
+ }>, Schema.Never>;
47
47
 
48
48
  export declare const markUnreadDescriptor: MutationProcedureDescriptor<"notifications.markUnread", Schema.Struct<{
49
- id: typeof Schema.String;
49
+ readonly id: Schema.String;
50
50
  }>, Schema.Struct<{
51
- ok: typeof Schema.Boolean;
52
- }>, typeof Schema.Never>;
51
+ readonly ok: Schema.Boolean;
52
+ }>, Schema.Never>;
53
53
 
54
54
  /** Declared for the codegen (`VoltroPlugin.rpcClientDescriptors`) so each tag is
55
55
  * emitted into the generated client group. Kept in lockstep with the exports. */
56
56
  export declare const notificationsRpcClientImports: ReadonlyArray<PluginRpcClientDescriptor>;
57
57
 
58
- export declare const preferencesDescriptor: QueryProcedureDescriptor<"notifications.preferences", Schema.Struct<{}>, Schema.Array$<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>, typeof Schema.Never>;
58
+ export declare const preferencesDescriptor: QueryProcedureDescriptor<"notifications.preferences", Schema.Struct<{}>, Schema.$Array<Schema.$Record<Schema.String, Schema.Unknown>>, Schema.Never>;
59
59
 
60
60
  export declare const setPreferenceDescriptor: MutationProcedureDescriptor<"notifications.setPreference", Schema.Struct<{
61
- category: typeof Schema.String;
62
- channel: typeof Schema.String;
63
- enabled: typeof Schema.Boolean;
61
+ readonly category: Schema.String;
62
+ readonly channel: Schema.String;
63
+ readonly enabled: Schema.Boolean;
64
64
  }>, Schema.Struct<{
65
- ok: typeof Schema.Boolean;
66
- }>, typeof Schema.Never>;
65
+ readonly ok: Schema.Boolean;
66
+ }>, Schema.Never>;
67
67
 
68
68
  export declare const setQuietHoursDescriptor: MutationProcedureDescriptor<"notifications.setQuietHours", Schema.Struct<{
69
- startMinute: typeof Schema.Number;
70
- endMinute: typeof Schema.Number;
71
- tz: Schema.optional<typeof Schema.String>;
72
- policy: Schema.optional<Schema.Literal<["hold", "drop"]>>;
69
+ readonly startMinute: Schema.Number;
70
+ readonly endMinute: Schema.Number;
71
+ readonly tz: Schema.optional<Schema.String>;
72
+ readonly policy: Schema.optional<Schema.Literals<readonly ["hold", "drop"]>>;
73
73
  }>, Schema.Struct<{
74
- ok: typeof Schema.Boolean;
75
- }>, typeof Schema.Never>;
74
+ readonly ok: Schema.Boolean;
75
+ }>, Schema.Never>;
76
76
 
77
77
  export declare const subscribeDescriptor: MutationProcedureDescriptor<"notifications.subscribe", Schema.Struct<{
78
- topic: typeof Schema.String;
78
+ readonly topic: Schema.String;
79
79
  }>, Schema.Struct<{
80
- ok: typeof Schema.Boolean;
81
- }>, typeof Schema.Never>;
80
+ readonly ok: Schema.Boolean;
81
+ }>, Schema.Never>;
82
82
 
83
83
  export declare const unarchiveDescriptor: MutationProcedureDescriptor<"notifications.unarchive", Schema.Struct<{
84
- id: typeof Schema.String;
84
+ readonly id: Schema.String;
85
85
  }>, Schema.Struct<{
86
- ok: typeof Schema.Boolean;
87
- }>, typeof Schema.Never>;
86
+ readonly ok: Schema.Boolean;
87
+ }>, Schema.Never>;
88
88
 
89
89
  export declare const unreadCountDescriptor: QueryProcedureDescriptor<"notifications.unreadCount", Schema.Struct<{}>, Schema.Struct<{
90
- count: typeof Schema.Number;
91
- }>, typeof Schema.Never>;
90
+ readonly count: Schema.Number;
91
+ }>, Schema.Never>;
92
92
 
93
93
  export declare const unsubscribeDescriptor: MutationProcedureDescriptor<"notifications.unsubscribe", Schema.Struct<{
94
- topic: typeof Schema.String;
94
+ readonly topic: Schema.String;
95
95
  }>, Schema.Struct<{
96
- ok: typeof Schema.Boolean;
97
- }>, typeof Schema.Never>;
96
+ readonly ok: Schema.Boolean;
97
+ }>, Schema.Never>;
98
98
 
99
99
  export declare const webPushPublicKeyDescriptor: QueryProcedureDescriptor<"notifications.webPushPublicKey", Schema.Struct<{}>, Schema.Struct<{
100
- key: typeof Schema.String;
101
- }>, typeof Schema.Never>;
100
+ readonly key: Schema.String;
101
+ }>, Schema.Never>;
102
102
 
103
103
  /** Registered only when the app configures a web-push channel — see
104
104
  * `notificationsPlugin`. */
105
105
  export declare const webPushRpcClientImports: ReadonlyArray<PluginRpcClientDescriptor>;
106
106
 
107
107
  export declare const webPushStatusDescriptor: QueryProcedureDescriptor<"notifications.webPushStatus", Schema.Struct<{}>, Schema.Struct<{
108
- endpoints: typeof Schema.Number;
109
- }>, typeof Schema.Never>;
108
+ readonly endpoints: Schema.Number;
109
+ }>, Schema.Never>;
110
110
 
111
111
  export declare const webPushSubscribeDescriptor: MutationProcedureDescriptor<"notifications.webPushSubscribe", Schema.Struct<{
112
- endpoint: typeof Schema.String;
113
- p256dh: typeof Schema.String;
114
- auth: typeof Schema.String;
115
- ua: Schema.optional<typeof Schema.String>;
112
+ readonly endpoint: Schema.String;
113
+ readonly p256dh: Schema.String;
114
+ readonly auth: Schema.String;
115
+ readonly ua: Schema.optional<Schema.String>;
116
116
  }>, Schema.Struct<{
117
- ok: typeof Schema.Boolean;
118
- }>, typeof Schema.Never>;
117
+ readonly ok: Schema.Boolean;
118
+ }>, Schema.Never>;
119
119
 
120
120
  export declare const webPushUnsubscribeDescriptor: MutationProcedureDescriptor<"notifications.webPushUnsubscribe", Schema.Struct<{
121
- endpoint: typeof Schema.String;
121
+ readonly endpoint: Schema.String;
122
122
  }>, Schema.Struct<{
123
- ok: typeof Schema.Boolean;
124
- }>, typeof Schema.Never>;
123
+ readonly ok: Schema.Boolean;
124
+ }>, Schema.Never>;
125
125
 
126
126
  export { }
package/dist/rpc.js CHANGED
@@ -8,10 +8,7 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
8
8
  unreadOnly: e.optional(e.Boolean),
9
9
  limit: e.optional(e.Number)
10
10
  }),
11
- output: e.Array(e.Record({
12
- key: e.String,
13
- value: e.Unknown
14
- })),
11
+ output: e.Array(e.Record(e.String, e.Unknown)),
15
12
  openAccess: "self-scoped read: returns only the calling subject's own inbox items"
16
13
  }), s = n({
17
14
  name: "notifications.unreadCount",
@@ -47,10 +44,7 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
47
44
  }), p = n({
48
45
  name: "notifications.preferences",
49
46
  input: e.Struct({}),
50
- output: e.Array(e.Record({
51
- key: e.String,
52
- value: e.Unknown
53
- })),
47
+ output: e.Array(e.Record(e.String, e.Unknown)),
54
48
  openAccess: "self-scoped read: returns only the calling subject's own channel preferences"
55
49
  }), m = t({
56
50
  name: "notifications.setPreference",
@@ -77,7 +71,7 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
77
71
  startMinute: e.Number,
78
72
  endMinute: e.Number,
79
73
  tz: e.optional(e.String),
80
- policy: e.optional(e.Literal("hold", "drop"))
74
+ policy: e.optional(e.Literals(["hold", "drop"]))
81
75
  }),
82
76
  output: e.Struct({ ok: e.Boolean }),
83
77
  openAccess: "self-scoped write: sets only the calling subject's own quiet-hours window"
@@ -0,0 +1,42 @@
1
+ import { Schema as e } from "effect";
2
+ //#region src/sendNotification.ts
3
+ var t = (e) => {
4
+ if (typeof e == "object" && e) {
5
+ let t = e;
6
+ if (typeof t.userId == "string") return [t.userId];
7
+ if (Array.isArray(t.userIds)) return t.userIds.filter((e) => typeof e == "string");
8
+ }
9
+ throw Error("sendNotification: pass `recipients` — the audience has neither `userId` nor `userIds`");
10
+ }, n = async (n, i, a) => {
11
+ let o = e.decodeUnknownSync(i.audience)(a.audience), s = e.decodeUnknownSync(i.payload)(a.payload), c = a.recipients ? await a.recipients(o) : t(o), l = [];
12
+ for (let e of c) {
13
+ let t = (a.localeFor ? await a.localeFor(e) : null) ?? a.defaultLocale ?? "en", o = await n.send(r(i, s, e, t, a.tenantId));
14
+ l.push({
15
+ recipientId: e,
16
+ locale: t,
17
+ result: o
18
+ });
19
+ }
20
+ return l;
21
+ }, r = (e, t, n, r, i) => {
22
+ let a = e.render(t, {
23
+ locale: r,
24
+ recipientId: n
25
+ });
26
+ return {
27
+ to: n,
28
+ category: e.category,
29
+ title: a.title,
30
+ body: a.body,
31
+ data: {
32
+ ...a.data,
33
+ notification: e.name,
34
+ ...a.image === void 0 ? {} : { image: a.image },
35
+ ...a.actions === void 0 ? {} : { actions: a.actions }
36
+ },
37
+ ...e.channels === void 0 ? {} : { channels: e.channels },
38
+ ...i === void 0 ? {} : { tenantId: i }
39
+ };
40
+ };
41
+ //#endregion
42
+ export { t as defaultRecipients, r as sendInputFor, n as sendNotification };
@@ -0,0 +1,212 @@
1
+ import { resolveChannels as e } from "./channels.js";
2
+ import { randomUUID as t } from "node:crypto";
3
+ //#region src/service.ts
4
+ var n = (e) => ({
5
+ id: "inApp",
6
+ deliver: async (n) => {
7
+ await e.addInbox({
8
+ id: t(),
9
+ subjectId: n.to,
10
+ category: n.category,
11
+ title: n.title,
12
+ body: n.body,
13
+ data: n.data ?? {},
14
+ tenantId: n.tenantId ?? null,
15
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
16
+ });
17
+ }
18
+ }), r = (e, t) => {
19
+ try {
20
+ let n = new Intl.DateTimeFormat("en-US", {
21
+ timeZone: t,
22
+ hour: "2-digit",
23
+ minute: "2-digit",
24
+ hour12: !1
25
+ }).formatToParts(e), r = Number(n.find((e) => e.type === "hour")?.value ?? "0") % 24, i = Number(n.find((e) => e.type === "minute")?.value ?? "0");
26
+ return r * 60 + i;
27
+ } catch {
28
+ return e.getUTCHours() * 60 + e.getUTCMinutes();
29
+ }
30
+ }, i = (e, t) => {
31
+ if (e.startMinute === e.endMinute) return !1;
32
+ let n = r(t, e.tz ?? "UTC");
33
+ return e.startMinute < e.endMinute ? n >= e.startMinute && n < e.endMinute : n >= e.startMinute || n < e.endMinute;
34
+ }, a = (e, t) => {
35
+ let n = r(t, e.tz ?? "UTC"), i = e.endMinute - n;
36
+ return i <= 0 && (i += 1440), new Date(t.getTime() + i * 6e4);
37
+ }, o = (n) => {
38
+ let r = new Map(n.channels.map((e) => [e.id, e])), o = n.channels.map((e) => e.id), c = n.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()), l = n.digestWindowMs ?? 0, u = async (i) => {
39
+ let a = await n.store.getPreferences(i.to), s = e(o, i.category, i.channels, a, i.to), l = o.filter((e) => !s.includes(e)), u = [], d = [], f = {
40
+ to: i.to,
41
+ category: i.category,
42
+ title: i.title,
43
+ body: i.body,
44
+ ...i.data ? { data: i.data } : {},
45
+ tenantId: i.tenantId ?? null
46
+ };
47
+ for (let e of s) {
48
+ let a = r.get(e);
49
+ if (!a) continue;
50
+ if (a.deliverDetailed !== void 0) {
51
+ try {
52
+ let r = await a.deliverDetailed(f);
53
+ for (let a of r) await n.store.recordDelivery({
54
+ id: t(),
55
+ to: i.to,
56
+ category: i.category,
57
+ channel: e,
58
+ at: c(),
59
+ status: a.status,
60
+ endpoint: a.endpoint,
61
+ ...a.error === void 0 ? {} : { error: a.error },
62
+ ...a.clickToken === void 0 ? {} : { clickToken: a.clickToken }
63
+ });
64
+ let o = r.filter((e) => e.status === "failed");
65
+ r.length === 0 || o.length < r.length ? u.push(e) : d.push({
66
+ channel: e,
67
+ error: o[0]?.error ?? "all endpoints failed"
68
+ });
69
+ } catch (r) {
70
+ let a = r instanceof Error ? r.message : String(r);
71
+ d.push({
72
+ channel: e,
73
+ error: a
74
+ }), await n.store.recordDelivery({
75
+ id: t(),
76
+ to: i.to,
77
+ category: i.category,
78
+ channel: e,
79
+ at: c(),
80
+ status: "failed",
81
+ error: a
82
+ });
83
+ }
84
+ continue;
85
+ }
86
+ let o = {
87
+ id: t(),
88
+ to: i.to,
89
+ category: i.category,
90
+ channel: e,
91
+ at: c()
92
+ };
93
+ try {
94
+ await a.deliver(f), u.push(e), await n.store.recordDelivery({
95
+ ...o,
96
+ status: "sent"
97
+ });
98
+ } catch (t) {
99
+ let r = t instanceof Error ? t.message : String(t);
100
+ d.push({
101
+ channel: e,
102
+ error: r
103
+ }), await n.store.recordDelivery({
104
+ ...o,
105
+ status: "failed",
106
+ error: r
107
+ });
108
+ }
109
+ }
110
+ return {
111
+ delivered: u,
112
+ failed: d,
113
+ skipped: l
114
+ };
115
+ }, d = async (e) => {
116
+ let r = await n.store.getQuietHours(e.to), o = new Date(c());
117
+ if (r && i(r, o)) return (r.policy ?? "hold") === "drop" ? {
118
+ delivered: [],
119
+ failed: [],
120
+ skipped: [],
121
+ held: "quiet-drop"
122
+ } : (await n.store.enqueueHeld({
123
+ id: t(),
124
+ kind: "quiet",
125
+ subjectId: e.to,
126
+ send: e,
127
+ flushAt: a(r, o).toISOString()
128
+ }), {
129
+ delivered: [],
130
+ failed: [],
131
+ skipped: [],
132
+ held: "quiet-held"
133
+ });
134
+ if (l > 0 && !e.channels) {
135
+ let r = (await n.store.pendingDigest(e.to))[0]?.flushAt ?? new Date(o.getTime() + l).toISOString();
136
+ return await n.store.enqueueHeld({
137
+ id: t(),
138
+ kind: "digest",
139
+ subjectId: e.to,
140
+ send: e,
141
+ flushAt: r
142
+ }), {
143
+ delivered: [],
144
+ failed: [],
145
+ skipped: [],
146
+ held: "digest"
147
+ };
148
+ }
149
+ return u(e);
150
+ };
151
+ return {
152
+ send: d,
153
+ inbox: (e, t) => n.store.listInbox(e, t),
154
+ unreadCount: (e) => n.store.unreadCount(e),
155
+ markRead: (e, t) => n.store.markRead(e, t),
156
+ markUnread: (e, t) => n.store.markUnread(e, t),
157
+ markAllRead: (e) => n.store.markAllRead(e),
158
+ setArchived: (e, t, r) => n.store.setArchived(e, t, r),
159
+ getPreferences: (e) => n.store.getPreferences(e),
160
+ setPreference: (e) => n.store.setPreference(e),
161
+ subscribe: (e, t, r) => n.store.subscribeTopic({
162
+ topic: e,
163
+ subjectId: t,
164
+ tenantId: r ?? null
165
+ }),
166
+ unsubscribe: (e, t) => n.store.unsubscribeTopic(e, t),
167
+ broadcast: async (e, t) => {
168
+ let r = await n.store.listTopicSubscribers(e), i = [];
169
+ for (let e of r) i.push(await d({
170
+ ...t,
171
+ to: e.subjectId,
172
+ tenantId: t.tenantId ?? e.tenantId ?? null
173
+ }));
174
+ return {
175
+ recipients: r.length,
176
+ results: i
177
+ };
178
+ },
179
+ setQuietHours: (e) => n.store.setQuietHours(e),
180
+ clearQuietHours: (e) => n.store.clearQuietHours(e),
181
+ getQuietHours: (e) => n.store.getQuietHours(e),
182
+ flushDue: async (e = new Date(c())) => {
183
+ let t = await n.store.dueHeld(e.toISOString());
184
+ if (t.length === 0) return 0;
185
+ for (let e of t.filter((e) => e.kind === "quiet")) await u(e.send);
186
+ let r = /* @__PURE__ */ new Map();
187
+ for (let e of t.filter((e) => e.kind === "digest")) {
188
+ let t = r.get(e.subjectId) ?? [];
189
+ t.push(e), r.set(e.subjectId, t);
190
+ }
191
+ for (let [e, t] of r) await u(s(e, t));
192
+ return await n.store.clearHeld(t.map((e) => e.id)), t.length;
193
+ }
194
+ };
195
+ }, s = (e, t) => {
196
+ if (t.length === 1) return t[0].send;
197
+ let n = t.map((e) => ({
198
+ category: e.send.category,
199
+ title: e.send.title,
200
+ body: e.send.body
201
+ }));
202
+ return {
203
+ to: e,
204
+ category: "digest",
205
+ title: `${t.length} new notifications`,
206
+ body: n.map((e) => `• ${e.title}`).join("\n"),
207
+ data: { items: n },
208
+ tenantId: t[0].send.tenantId ?? null
209
+ };
210
+ };
211
+ //#endregion
212
+ export { o as buildNotificationService, n as inAppChannel, i as inQuietHours, r as minuteOfDayInZone, a as quietHoursEnd };