@house-rules/notifications 0.0.0-stage → 0.1.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.
@@ -0,0 +1,206 @@
1
+ import { type Actor } from "@house-rules/domain";
2
+ import { Effect, Schema } from "effect";
3
+ import type { NotificationStore } from "../storage.js";
4
+ import { type Channel, NotificationInputInvalid, Recipient } from "./notification.js";
5
+ import type { NotificationsOptions } from "./options.js";
6
+ export declare const makeNotifications: (store: NotificationStore["Service"], options: NotificationsOptions) => {
7
+ notify: (input: {
8
+ readonly recipients: readonly {
9
+ readonly kind: string;
10
+ readonly id: string;
11
+ }[];
12
+ readonly category: string;
13
+ readonly title: string;
14
+ readonly body: string;
15
+ readonly data: Schema.Json;
16
+ }) => Effect.Effect<{
17
+ notifications: {
18
+ readonly id: string;
19
+ readonly recipient: {
20
+ readonly kind: string;
21
+ readonly id: string;
22
+ };
23
+ readonly category: string;
24
+ readonly title: string;
25
+ readonly body: string;
26
+ readonly data: Schema.Json;
27
+ readonly createdAt: number;
28
+ readonly readAt: number | null;
29
+ }[];
30
+ events: {
31
+ kind: "NotificationCreated";
32
+ notificationId: string;
33
+ recipient: Recipient;
34
+ channels: Channel[];
35
+ }[];
36
+ }, import("@house-rules/domain").NoOpenUnit | NotificationInputInvalid | import("./notification.js").NotificationStoreUnavailable, never>;
37
+ listMine: (actor: {
38
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
39
+ }, page?: {
40
+ readonly size?: number;
41
+ readonly cursor?: {
42
+ readonly createdAt: number;
43
+ readonly id: string;
44
+ };
45
+ } | undefined) => Effect.Effect<{
46
+ notifications: {
47
+ readonly id: string;
48
+ readonly recipient: {
49
+ readonly kind: string;
50
+ readonly id: string;
51
+ };
52
+ readonly category: string;
53
+ readonly title: string;
54
+ readonly body: string;
55
+ readonly data: Schema.Json;
56
+ readonly createdAt: number;
57
+ readonly readAt: number | null;
58
+ }[];
59
+ next: {
60
+ createdAt: number;
61
+ id: string;
62
+ };
63
+ } | {
64
+ next?: never;
65
+ notifications: {
66
+ readonly id: string;
67
+ readonly recipient: {
68
+ readonly kind: string;
69
+ readonly id: string;
70
+ };
71
+ readonly category: string;
72
+ readonly title: string;
73
+ readonly body: string;
74
+ readonly data: Schema.Json;
75
+ readonly createdAt: number;
76
+ readonly readAt: number | null;
77
+ }[];
78
+ }, NotificationInputInvalid | import("./notification.js").NotificationStoreUnavailable, never>;
79
+ unreadCount: (actor: Actor) => Effect.Effect<number, import("./notification.js").NotificationStoreUnavailable, never>;
80
+ markRead: (actor: {
81
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
82
+ }, notificationId: string) => Effect.Effect<{
83
+ readonly notifications: readonly {
84
+ readonly id: string;
85
+ readonly recipient: {
86
+ readonly kind: string;
87
+ readonly id: string;
88
+ };
89
+ readonly category: string;
90
+ readonly title: string;
91
+ readonly body: string;
92
+ readonly data: Schema.Json;
93
+ readonly createdAt: number;
94
+ readonly readAt: number | null;
95
+ }[];
96
+ readonly events: readonly {
97
+ readonly kind: "NotificationRead";
98
+ readonly notificationId: string;
99
+ readonly recipient: {
100
+ readonly kind: string;
101
+ readonly id: string;
102
+ };
103
+ }[];
104
+ }, import("@house-rules/domain").NoOpenUnit | NotificationInputInvalid | import("./notification.js").NotificationStoreUnavailable, never>;
105
+ markAllRead: (actor: {
106
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
107
+ }) => Effect.Effect<{
108
+ readonly notifications: readonly {
109
+ readonly id: string;
110
+ readonly recipient: {
111
+ readonly kind: string;
112
+ readonly id: string;
113
+ };
114
+ readonly category: string;
115
+ readonly title: string;
116
+ readonly body: string;
117
+ readonly data: Schema.Json;
118
+ readonly createdAt: number;
119
+ readonly readAt: number | null;
120
+ }[];
121
+ readonly events: readonly {
122
+ readonly kind: "NotificationRead";
123
+ readonly notificationId: string;
124
+ readonly recipient: {
125
+ readonly kind: string;
126
+ readonly id: string;
127
+ };
128
+ }[];
129
+ }, import("@house-rules/domain").NoOpenUnit | import("./notification.js").NotificationStoreUnavailable, never>;
130
+ getMyPreferences: (actor: {
131
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
132
+ }) => Effect.Effect<{
133
+ preferences: {
134
+ category: string;
135
+ channel: "email" | "inapp" | "push";
136
+ mode: "daily" | "hourly" | "immediate" | "off";
137
+ }[];
138
+ quietHours: {
139
+ readonly start: string;
140
+ readonly end: string;
141
+ } | null;
142
+ timeZone: string | null;
143
+ }, import("./notification.js").NotificationStoreUnavailable, never>;
144
+ setMyPreference: (actor: {
145
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
146
+ }, preference: {
147
+ readonly category: string;
148
+ readonly channel: "email" | "inapp" | "push";
149
+ readonly mode: "daily" | "hourly" | "immediate" | "off";
150
+ }) => Effect.Effect<undefined, import("@house-rules/domain").NoOpenUnit | NotificationInputInvalid | import("./notification.js").NotificationStoreUnavailable, never>;
151
+ setMyQuietHours: (actor: {
152
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
153
+ }, hours: {
154
+ readonly start: string;
155
+ readonly end: string;
156
+ } | null) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | NotificationInputInvalid | import("./notification.js").NotificationStoreUnavailable, never>;
157
+ setMyTimeZone: (actor: {
158
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
159
+ }, timeZone: string | null) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | NotificationInputInvalid | import("./notification.js").NotificationStoreUnavailable, never>;
160
+ dueDeliveries: () => Effect.Effect<{
161
+ readonly recipient: {
162
+ readonly kind: string;
163
+ readonly id: string;
164
+ };
165
+ readonly channel: "email" | "inapp" | "push";
166
+ readonly mode: "daily" | "hourly" | "immediate";
167
+ readonly deliveries: readonly {
168
+ readonly delivery: {
169
+ readonly id: string;
170
+ readonly notificationId: string;
171
+ readonly recipient: {
172
+ readonly kind: string;
173
+ readonly id: string;
174
+ };
175
+ readonly channel: "email" | "inapp" | "push";
176
+ readonly mode: "daily" | "hourly" | "immediate";
177
+ readonly state: "cancelled" | "failed" | "held" | "pending" | "sent";
178
+ readonly dueAt: number;
179
+ readonly retryAt: number | null;
180
+ readonly sentAt: number | null;
181
+ readonly attempts: number;
182
+ readonly lastError: string | null;
183
+ };
184
+ readonly notification: {
185
+ readonly id: string;
186
+ readonly recipient: {
187
+ readonly kind: string;
188
+ readonly id: string;
189
+ };
190
+ readonly category: string;
191
+ readonly title: string;
192
+ readonly body: string;
193
+ readonly data: Schema.Json;
194
+ readonly createdAt: number;
195
+ readonly readAt: number | null;
196
+ };
197
+ }[];
198
+ }[], import("./notification.js").NotificationStoreUnavailable, never>;
199
+ markSent: (deliveryIds: readonly string[]) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | NotificationInputInvalid | import("./notification.js").NotificationStoreUnavailable, never>;
200
+ markFailed: (deliveryId: string, error: string) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | NotificationInputInvalid | import("./notification.js").NotificationStoreUnavailable, never>;
201
+ erase: (recipient: {
202
+ readonly kind: string;
203
+ readonly id: string;
204
+ }) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | NotificationInputInvalid | import("./notification.js").NotificationStoreUnavailable, never>;
205
+ };
206
+ export type NotificationsService = ReturnType<typeof makeNotifications>;
@@ -0,0 +1,224 @@
1
+ import { UnitOfWork } from "@house-rules/domain";
2
+ import { DateTime, Effect, Random, Schema } from "effect";
3
+ import { NotificationInputInvalid, NotificationPage, NotifyInput, Preference, QuietHours, Recipient, TimeZone, } from "./notification.js";
4
+ import { digestAt, releaseAt } from "./timing.js";
5
+ const invalid = () => new NotificationInputInvalid({ message: "Invalid notification input." });
6
+ const decode = (schema) => (value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(invalid));
7
+ const now = DateTime.now.pipe(Effect.map(DateTime.toEpochMillis));
8
+ const newId = Effect.all(Array.from({ length: 4 }, () => Random.nextIntBetween(0, 0xffff_ffff))).pipe(Effect.map((words) => words.map((word) => word.toString(16).padStart(8, "0")).join("")));
9
+ const readResult = (notifications) => ({
10
+ notifications,
11
+ events: notifications.map((n) => ({
12
+ kind: "NotificationRead",
13
+ notificationId: n.id,
14
+ recipient: n.recipient,
15
+ })),
16
+ });
17
+ export const makeNotifications = (store, options) => {
18
+ const mine = (actor) => ({ kind: options.recipientKind, id: actor.id });
19
+ const categoryOf = (name) => options.categories.find((category) => category.name === name);
20
+ const modeFor = (category, channel, preferences) => {
21
+ const saved = preferences.find((p) => p.category === category.name && p.channel === channel);
22
+ if (channel === "inapp")
23
+ return "immediate";
24
+ if (category.mandatory)
25
+ return category.defaultChannels.includes(channel) ||
26
+ (saved !== undefined && saved.mode !== "off")
27
+ ? "immediate"
28
+ : "off";
29
+ return (saved?.mode ??
30
+ (category.defaultChannels.includes(channel) ? "immediate" : "off"));
31
+ };
32
+ const refresh = Effect.fn("Notifications.refresh")(function* (recipient) {
33
+ const at = yield* now;
34
+ const preferences = yield* store.preferences(recipient);
35
+ const hours = yield* store.quietHours(recipient);
36
+ const timeZone = (yield* store.timeZone(recipient)) ?? "UTC";
37
+ for (const { delivery, notification } of yield* store.deliveries(recipient, {
38
+ unsettledOnly: true,
39
+ })) {
40
+ const category = categoryOf(notification.category);
41
+ if (category === undefined ||
42
+ category.mandatory ||
43
+ delivery.channel === "inapp" ||
44
+ (delivery.state !== "pending" && delivery.state !== "held"))
45
+ continue;
46
+ const mode = modeFor(category, delivery.channel, preferences);
47
+ if (mode === "off") {
48
+ yield* store.reschedule(delivery.id, { ...delivery, state: "cancelled" });
49
+ continue;
50
+ }
51
+ // Keep elapsed digest times due, and never shorten an outstanding retry backoff.
52
+ const base = Math.max(at, digestAt(notification.createdAt, mode, timeZone, options.dailyDigestTime ?? "08:00"), delivery.retryAt ?? 0);
53
+ const dueAt = hours === null ? base : releaseAt(base, hours, timeZone);
54
+ yield* store.reschedule(delivery.id, {
55
+ mode,
56
+ dueAt,
57
+ state: dueAt > base ? "held" : "pending",
58
+ });
59
+ }
60
+ });
61
+ return {
62
+ notify: Effect.fn("Notifications.notify")(function* (input) {
63
+ yield* UnitOfWork.required;
64
+ const decoded = yield* decode(NotifyInput)(input);
65
+ const category = categoryOf(decoded.category);
66
+ if (category === undefined)
67
+ return yield* invalid();
68
+ const at = yield* now;
69
+ const notifications = [];
70
+ const events = [];
71
+ const seen = new Set();
72
+ for (const recipient of decoded.recipients) {
73
+ const key = JSON.stringify([recipient.kind, recipient.id]);
74
+ if (seen.has(key))
75
+ continue;
76
+ seen.add(key);
77
+ const preferences = yield* store.preferences(recipient);
78
+ const hours = yield* store.quietHours(recipient);
79
+ const timeZone = (yield* store.timeZone(recipient)) ?? "UTC";
80
+ const notification = {
81
+ id: yield* newId,
82
+ recipient,
83
+ category: category.name,
84
+ title: decoded.title,
85
+ body: decoded.body,
86
+ data: decoded.data,
87
+ createdAt: at,
88
+ readAt: null,
89
+ };
90
+ const deliveries = [];
91
+ const channels = [];
92
+ for (const channel of options.channels) {
93
+ const mode = modeFor(category, channel, preferences);
94
+ if (mode === "off")
95
+ continue;
96
+ channels.push(channel);
97
+ // The inbox row is the in-app delivery, not unsettled work for a sender.
98
+ if (channel === "inapp")
99
+ continue;
100
+ const base = digestAt(at, mode, timeZone, options.dailyDigestTime ?? "08:00");
101
+ const dueAt = !category.mandatory && hours !== null ? releaseAt(base, hours, timeZone) : base;
102
+ deliveries.push({
103
+ id: yield* newId,
104
+ notificationId: notification.id,
105
+ recipient,
106
+ channel,
107
+ mode,
108
+ state: dueAt > base ? "held" : "pending",
109
+ dueAt,
110
+ retryAt: null,
111
+ sentAt: null,
112
+ attempts: 0,
113
+ lastError: null,
114
+ });
115
+ }
116
+ yield* store.insert(notification, deliveries);
117
+ notifications.push(notification);
118
+ events.push({
119
+ kind: "NotificationCreated",
120
+ notificationId: notification.id,
121
+ recipient,
122
+ channels,
123
+ });
124
+ }
125
+ return { notifications, events };
126
+ }),
127
+ listMine: Effect.fn("Notifications.listMine")(function* (actor, page) {
128
+ const decoded = yield* decode(NotificationPage)(page ?? {});
129
+ const size = Math.min(decoded.size ?? options.maxPageSize, options.maxPageSize);
130
+ const rows = yield* store.list(mine(actor), {
131
+ size: size + 1,
132
+ ...(decoded.cursor === undefined ? {} : { after: decoded.cursor }),
133
+ });
134
+ const notifications = rows.slice(0, size);
135
+ const last = notifications[notifications.length - 1];
136
+ return rows.length > size && last !== undefined
137
+ ? { notifications, next: { createdAt: last.createdAt, id: last.id } }
138
+ : { notifications };
139
+ }),
140
+ unreadCount: (actor) => store.unreadCount(mine(actor)),
141
+ markRead: Effect.fn("Notifications.markRead")(function* (actor, notificationId) {
142
+ yield* UnitOfWork.required;
143
+ const id = yield* decode(Schema.NonEmptyString)(notificationId);
144
+ return readResult(yield* store.markRead(mine(actor), yield* now, id));
145
+ }),
146
+ markAllRead: Effect.fn("Notifications.markAllRead")(function* (actor) {
147
+ yield* UnitOfWork.required;
148
+ return readResult(yield* store.markRead(mine(actor), yield* now));
149
+ }),
150
+ getMyPreferences: Effect.fn("Notifications.getMyPreferences")(function* (actor) {
151
+ const saved = yield* store.preferences(mine(actor));
152
+ return {
153
+ preferences: options.categories.flatMap((category) => options.channels.map((channel) => ({
154
+ category: category.name,
155
+ channel,
156
+ mode: modeFor(category, channel, saved),
157
+ }))),
158
+ quietHours: yield* store.quietHours(mine(actor)),
159
+ timeZone: yield* store.timeZone(mine(actor)),
160
+ };
161
+ }),
162
+ setMyPreference: Effect.fn("Notifications.setMyPreference")(function* (actor, preference) {
163
+ yield* UnitOfWork.required;
164
+ const decoded = yield* decode(Preference)(preference);
165
+ const category = categoryOf(decoded.category);
166
+ if (category === undefined || !options.channels.includes(decoded.channel))
167
+ return yield* invalid();
168
+ if ((decoded.channel === "inapp" || category.mandatory) && decoded.mode !== "immediate")
169
+ return yield* invalid();
170
+ yield* store.setPreference(mine(actor), decoded);
171
+ yield* refresh(mine(actor));
172
+ }),
173
+ setMyQuietHours: Effect.fn("Notifications.setMyQuietHours")(function* (actor, hours) {
174
+ yield* UnitOfWork.required;
175
+ const decoded = yield* decode(Schema.NullOr(QuietHours))(hours);
176
+ yield* store.setQuietHours(mine(actor), decoded);
177
+ yield* refresh(mine(actor));
178
+ }),
179
+ setMyTimeZone: Effect.fn("Notifications.setMyTimeZone")(function* (actor, timeZone) {
180
+ yield* UnitOfWork.required;
181
+ yield* store.setTimeZone(mine(actor), yield* decode(Schema.NullOr(TimeZone))(timeZone));
182
+ yield* refresh(mine(actor));
183
+ }),
184
+ dueDeliveries: Effect.fn("Notifications.dueDeliveries")(function* () {
185
+ const at = yield* now;
186
+ const rows = yield* store.due(at, options.maxDueBatch ?? 100);
187
+ const groups = new Map();
188
+ for (const row of rows) {
189
+ const { delivery } = row;
190
+ const recipientKey = JSON.stringify([delivery.recipient.kind, delivery.recipient.id]);
191
+ const key = delivery.mode === "immediate"
192
+ ? delivery.id
193
+ : JSON.stringify([recipientKey, delivery.channel, delivery.mode]);
194
+ const group = groups.get(key);
195
+ groups.set(key, {
196
+ recipient: delivery.recipient,
197
+ channel: delivery.channel,
198
+ mode: group?.mode ?? delivery.mode,
199
+ deliveries: [...(group?.deliveries ?? []), row],
200
+ });
201
+ }
202
+ return [...groups.values()];
203
+ }),
204
+ markSent: Effect.fn("Notifications.markSent")(function* (deliveryIds) {
205
+ yield* UnitOfWork.required;
206
+ const ids = yield* decode(Schema.Array(Schema.NonEmptyString))(deliveryIds);
207
+ yield* store.markSent(ids, yield* now);
208
+ }),
209
+ markFailed: Effect.fn("Notifications.markFailed")(function* (deliveryId, error) {
210
+ yield* UnitOfWork.required;
211
+ const id = yield* decode(Schema.NonEmptyString)(deliveryId);
212
+ const detail = yield* decode(Schema.String)(error);
213
+ yield* store.markFailed(id, detail, yield* now, {
214
+ maxAttempts: options.maxAttempts ?? 5,
215
+ baseMs: options.retryBaseMs ?? 1000,
216
+ capMs: options.retryCapMs ?? 3_600_000,
217
+ });
218
+ }),
219
+ erase: Effect.fn("Notifications.erase")(function* (recipient) {
220
+ yield* UnitOfWork.required;
221
+ yield* store.erase(yield* decode(Recipient)(recipient));
222
+ }),
223
+ };
224
+ };
@@ -0,0 +1,185 @@
1
+ import { Schema } from "effect";
2
+ import { NotificationForbidden, NotificationInputInvalid, NotificationStoreUnavailable } from "./notification.js";
3
+ export declare const NotificationsOperations: {
4
+ readonly notify: import("@house-rules/domain/contract").Operation<"Notifications.notify", Schema.Struct<{
5
+ readonly recipients: Schema.$Array<Schema.Struct<{
6
+ readonly kind: Schema.NonEmptyString;
7
+ readonly id: Schema.NonEmptyString;
8
+ }>>;
9
+ readonly category: Schema.NonEmptyString;
10
+ readonly title: Schema.NonEmptyString;
11
+ readonly body: Schema.String;
12
+ readonly data: Schema.Codec<Schema.Json, Schema.Json, never, never>;
13
+ }>, Schema.Struct<{
14
+ readonly notifications: Schema.$Array<Schema.Struct<{
15
+ readonly id: Schema.NonEmptyString;
16
+ readonly recipient: Schema.Struct<{
17
+ readonly kind: Schema.NonEmptyString;
18
+ readonly id: Schema.NonEmptyString;
19
+ }>;
20
+ readonly category: Schema.NonEmptyString;
21
+ readonly title: Schema.NonEmptyString;
22
+ readonly body: Schema.String;
23
+ readonly data: Schema.Codec<Schema.Json, Schema.Json, never, never>;
24
+ readonly createdAt: Schema.Int;
25
+ readonly readAt: Schema.NullOr<Schema.Int>;
26
+ }>>;
27
+ readonly events: Schema.$Array<Schema.Struct<{
28
+ readonly kind: Schema.Literal<"NotificationCreated">;
29
+ readonly notificationId: Schema.NonEmptyString;
30
+ readonly recipient: Schema.Struct<{
31
+ readonly kind: Schema.NonEmptyString;
32
+ readonly id: Schema.NonEmptyString;
33
+ }>;
34
+ readonly channels: Schema.$Array<Schema.Literals<readonly ["inapp", "push", "email"]>>;
35
+ }>>;
36
+ }>, Schema.Union<readonly [typeof NotificationForbidden, typeof NotificationInputInvalid, typeof NotificationStoreUnavailable]>, NoInfer<readonly []>, false, false, true, false>;
37
+ readonly listMine: import("@house-rules/domain/contract").Operation<"Notifications.listMine", Schema.Struct<{
38
+ readonly page: Schema.optionalKey<Schema.Struct<{
39
+ readonly size: Schema.optionalKey<Schema.Int>;
40
+ readonly cursor: Schema.optionalKey<Schema.Struct<{
41
+ readonly createdAt: Schema.Int;
42
+ readonly id: Schema.NonEmptyString;
43
+ }>>;
44
+ }>>;
45
+ }>, Schema.Struct<{
46
+ readonly notifications: Schema.$Array<Schema.Struct<{
47
+ readonly id: Schema.NonEmptyString;
48
+ readonly recipient: Schema.Struct<{
49
+ readonly kind: Schema.NonEmptyString;
50
+ readonly id: Schema.NonEmptyString;
51
+ }>;
52
+ readonly category: Schema.NonEmptyString;
53
+ readonly title: Schema.NonEmptyString;
54
+ readonly body: Schema.String;
55
+ readonly data: Schema.Codec<Schema.Json, Schema.Json, never, never>;
56
+ readonly createdAt: Schema.Int;
57
+ readonly readAt: Schema.NullOr<Schema.Int>;
58
+ }>>;
59
+ readonly next: Schema.optionalKey<Schema.Struct<{
60
+ readonly createdAt: Schema.Int;
61
+ readonly id: Schema.NonEmptyString;
62
+ }>>;
63
+ }>, Schema.Union<readonly [typeof NotificationForbidden, typeof NotificationInputInvalid, typeof NotificationStoreUnavailable]>, NoInfer<readonly []>, true, false, false, false>;
64
+ readonly unreadCount: import("@house-rules/domain/contract").Operation<"Notifications.unreadCount", Schema.$Record<Schema.String, Schema.Never>, Schema.Int, Schema.Union<readonly [typeof NotificationForbidden, typeof NotificationInputInvalid, typeof NotificationStoreUnavailable]>, NoInfer<readonly []>, true, false, false, false>;
65
+ readonly markRead: import("@house-rules/domain/contract").Operation<"Notifications.markRead", Schema.Struct<{
66
+ readonly notificationId: Schema.NonEmptyString;
67
+ }>, Schema.Struct<{
68
+ readonly notifications: Schema.$Array<Schema.Struct<{
69
+ readonly id: Schema.NonEmptyString;
70
+ readonly recipient: Schema.Struct<{
71
+ readonly kind: Schema.NonEmptyString;
72
+ readonly id: Schema.NonEmptyString;
73
+ }>;
74
+ readonly category: Schema.NonEmptyString;
75
+ readonly title: Schema.NonEmptyString;
76
+ readonly body: Schema.String;
77
+ readonly data: Schema.Codec<Schema.Json, Schema.Json, never, never>;
78
+ readonly createdAt: Schema.Int;
79
+ readonly readAt: Schema.NullOr<Schema.Int>;
80
+ }>>;
81
+ readonly events: Schema.$Array<Schema.Struct<{
82
+ readonly kind: Schema.Literal<"NotificationRead">;
83
+ readonly notificationId: Schema.NonEmptyString;
84
+ readonly recipient: Schema.Struct<{
85
+ readonly kind: Schema.NonEmptyString;
86
+ readonly id: Schema.NonEmptyString;
87
+ }>;
88
+ }>>;
89
+ }>, Schema.Union<readonly [typeof NotificationForbidden, typeof NotificationInputInvalid, typeof NotificationStoreUnavailable]>, NoInfer<readonly []>, false, false, true, false>;
90
+ readonly markAllRead: import("@house-rules/domain/contract").Operation<"Notifications.markAllRead", Schema.$Record<Schema.String, Schema.Never>, Schema.Struct<{
91
+ readonly notifications: Schema.$Array<Schema.Struct<{
92
+ readonly id: Schema.NonEmptyString;
93
+ readonly recipient: Schema.Struct<{
94
+ readonly kind: Schema.NonEmptyString;
95
+ readonly id: Schema.NonEmptyString;
96
+ }>;
97
+ readonly category: Schema.NonEmptyString;
98
+ readonly title: Schema.NonEmptyString;
99
+ readonly body: Schema.String;
100
+ readonly data: Schema.Codec<Schema.Json, Schema.Json, never, never>;
101
+ readonly createdAt: Schema.Int;
102
+ readonly readAt: Schema.NullOr<Schema.Int>;
103
+ }>>;
104
+ readonly events: Schema.$Array<Schema.Struct<{
105
+ readonly kind: Schema.Literal<"NotificationRead">;
106
+ readonly notificationId: Schema.NonEmptyString;
107
+ readonly recipient: Schema.Struct<{
108
+ readonly kind: Schema.NonEmptyString;
109
+ readonly id: Schema.NonEmptyString;
110
+ }>;
111
+ }>>;
112
+ }>, Schema.Union<readonly [typeof NotificationForbidden, typeof NotificationInputInvalid, typeof NotificationStoreUnavailable]>, NoInfer<readonly []>, false, false, true, false>;
113
+ readonly getMyPreferences: import("@house-rules/domain/contract").Operation<"Notifications.getMyPreferences", Schema.$Record<Schema.String, Schema.Never>, Schema.Struct<{
114
+ readonly preferences: Schema.$Array<Schema.Struct<{
115
+ readonly category: Schema.NonEmptyString;
116
+ readonly channel: Schema.Literals<readonly ["inapp", "push", "email"]>;
117
+ readonly mode: Schema.Literals<readonly ["immediate", "hourly", "daily", "off"]>;
118
+ }>>;
119
+ readonly quietHours: Schema.NullOr<Schema.Struct<{
120
+ readonly start: Schema.String;
121
+ readonly end: Schema.String;
122
+ }>>;
123
+ readonly timeZone: Schema.NullOr<Schema.NonEmptyString>;
124
+ }>, Schema.Union<readonly [typeof NotificationForbidden, typeof NotificationInputInvalid, typeof NotificationStoreUnavailable]>, NoInfer<readonly []>, true, false, false, false>;
125
+ readonly setMyPreference: import("@house-rules/domain/contract").Operation<"Notifications.setMyPreference", Schema.Struct<{
126
+ readonly category: Schema.NonEmptyString;
127
+ readonly channel: Schema.Literals<readonly ["inapp", "push", "email"]>;
128
+ readonly mode: Schema.Literals<readonly ["immediate", "hourly", "daily", "off"]>;
129
+ }>, Schema.Void, Schema.Union<readonly [typeof NotificationForbidden, typeof NotificationInputInvalid, typeof NotificationStoreUnavailable]>, NoInfer<readonly []>, false, false, true, false>;
130
+ readonly setMyQuietHours: import("@house-rules/domain/contract").Operation<"Notifications.setMyQuietHours", Schema.Struct<{
131
+ readonly quietHours: Schema.NullOr<Schema.Struct<{
132
+ readonly start: Schema.String;
133
+ readonly end: Schema.String;
134
+ }>>;
135
+ }>, Schema.Void, Schema.Union<readonly [typeof NotificationForbidden, typeof NotificationInputInvalid, typeof NotificationStoreUnavailable]>, NoInfer<readonly []>, false, false, true, false>;
136
+ readonly setMyTimeZone: import("@house-rules/domain/contract").Operation<"Notifications.setMyTimeZone", Schema.Struct<{
137
+ readonly timeZone: Schema.NullOr<Schema.NonEmptyString>;
138
+ }>, Schema.Void, Schema.Union<readonly [typeof NotificationForbidden, typeof NotificationInputInvalid, typeof NotificationStoreUnavailable]>, NoInfer<readonly []>, false, false, true, false>;
139
+ readonly dueDeliveries: import("@house-rules/domain/contract").Operation<"Notifications.dueDeliveries", Schema.$Record<Schema.String, Schema.Never>, Schema.$Array<Schema.Struct<{
140
+ readonly recipient: Schema.Struct<{
141
+ readonly kind: Schema.NonEmptyString;
142
+ readonly id: Schema.NonEmptyString;
143
+ }>;
144
+ readonly channel: Schema.Literals<readonly ["inapp", "push", "email"]>;
145
+ readonly mode: Schema.Literals<readonly ["immediate", "hourly", "daily"]>;
146
+ readonly deliveries: Schema.$Array<Schema.Struct<{
147
+ readonly delivery: Schema.Struct<{
148
+ readonly id: Schema.NonEmptyString;
149
+ readonly notificationId: Schema.NonEmptyString;
150
+ readonly recipient: Schema.Struct<{
151
+ readonly kind: Schema.NonEmptyString;
152
+ readonly id: Schema.NonEmptyString;
153
+ }>;
154
+ readonly channel: Schema.Literals<readonly ["inapp", "push", "email"]>;
155
+ readonly mode: Schema.Literals<readonly ["immediate", "hourly", "daily"]>;
156
+ readonly state: Schema.Literals<readonly ["pending", "held", "sent", "failed", "cancelled"]>;
157
+ readonly dueAt: Schema.Int;
158
+ readonly retryAt: Schema.NullOr<Schema.Int>;
159
+ readonly sentAt: Schema.NullOr<Schema.Int>;
160
+ readonly attempts: Schema.Int;
161
+ readonly lastError: Schema.NullOr<Schema.String>;
162
+ }>;
163
+ readonly notification: Schema.Struct<{
164
+ readonly id: Schema.NonEmptyString;
165
+ readonly recipient: Schema.Struct<{
166
+ readonly kind: Schema.NonEmptyString;
167
+ readonly id: Schema.NonEmptyString;
168
+ }>;
169
+ readonly category: Schema.NonEmptyString;
170
+ readonly title: Schema.NonEmptyString;
171
+ readonly body: Schema.String;
172
+ readonly data: Schema.Codec<Schema.Json, Schema.Json, never, never>;
173
+ readonly createdAt: Schema.Int;
174
+ readonly readAt: Schema.NullOr<Schema.Int>;
175
+ }>;
176
+ }>>;
177
+ }>>, Schema.Union<readonly [typeof NotificationForbidden, typeof NotificationInputInvalid, typeof NotificationStoreUnavailable]>, NoInfer<readonly []>, true, false, false, false>;
178
+ readonly markSent: import("@house-rules/domain/contract").Operation<"Notifications.markSent", Schema.Struct<{
179
+ readonly deliveryIds: Schema.$Array<Schema.NonEmptyString>;
180
+ }>, Schema.Void, Schema.Union<readonly [typeof NotificationForbidden, typeof NotificationInputInvalid, typeof NotificationStoreUnavailable]>, NoInfer<readonly []>, false, false, true, false>;
181
+ readonly markFailed: import("@house-rules/domain/contract").Operation<"Notifications.markFailed", Schema.Struct<{
182
+ readonly deliveryId: Schema.NonEmptyString;
183
+ readonly error: Schema.String;
184
+ }>, Schema.Void, Schema.Union<readonly [typeof NotificationForbidden, typeof NotificationInputInvalid, typeof NotificationStoreUnavailable]>, NoInfer<readonly []>, false, false, true, false>;
185
+ };