@marktiderman/genesis-notifications 0.0.0-stage → 0.1.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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +251 -2
  3. package/dist/blocks.d.ts +10 -0
  4. package/dist/blocks.js +35 -0
  5. package/dist/blocks.js.map +1 -0
  6. package/dist/chunk-2BG336II.js +124 -0
  7. package/dist/chunk-2BG336II.js.map +1 -0
  8. package/dist/chunk-7PHA3WVG.js +51 -0
  9. package/dist/chunk-7PHA3WVG.js.map +1 -0
  10. package/dist/chunk-EWZB35V6.js +113 -0
  11. package/dist/chunk-EWZB35V6.js.map +1 -0
  12. package/dist/chunk-UXNWI7OS.js +118 -0
  13. package/dist/chunk-UXNWI7OS.js.map +1 -0
  14. package/dist/email-render-CID_pXDy.d.ts +53 -0
  15. package/dist/email.d.ts +46 -0
  16. package/dist/email.js +64 -0
  17. package/dist/email.js.map +1 -0
  18. package/dist/index.d.ts +26 -0
  19. package/dist/index.js +151 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/native.d.ts +92 -0
  22. package/dist/native.js +170 -0
  23. package/dist/native.js.map +1 -0
  24. package/dist/react.d.ts +98 -0
  25. package/dist/react.js +4 -0
  26. package/dist/react.js.map +1 -0
  27. package/dist/runner.d.ts +174 -0
  28. package/dist/runner.js +3 -0
  29. package/dist/runner.js.map +1 -0
  30. package/dist/server.d.ts +658 -0
  31. package/dist/server.js +995 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/supabase.d.ts +118 -0
  34. package/dist/supabase.js +3 -0
  35. package/dist/supabase.js.map +1 -0
  36. package/dist/types-BilguCPW.d.ts +332 -0
  37. package/dist/web.d.ts +79 -0
  38. package/dist/web.js +233 -0
  39. package/dist/web.js.map +1 -0
  40. package/functions/genesis-notifications/index.ts +60 -0
  41. package/functions/genesis-notifications/notifications.ts +18 -0
  42. package/migrations/0001_genesis_notifications.sql +2057 -0
  43. package/migrations/tests/0001_genesis_notifications.test.sql +473 -0
  44. package/package.json +191 -3
  45. package/src/blocks.ts +42 -0
  46. package/src/core/index.ts +43 -0
  47. package/src/core/registry.ts +354 -0
  48. package/src/core/types.ts +90 -0
  49. package/src/core/validate.ts +73 -0
  50. package/src/email/index.tsx +142 -0
  51. package/src/index.ts +6 -0
  52. package/src/native/index.tsx +283 -0
  53. package/src/react/index.tsx +226 -0
  54. package/src/runner/index.ts +299 -0
  55. package/src/server/crypto.ts +44 -0
  56. package/src/server/dns.ts +106 -0
  57. package/src/server/email-render.ts +90 -0
  58. package/src/server/errors.ts +61 -0
  59. package/src/server/expo.ts +133 -0
  60. package/src/server/handler.ts +127 -0
  61. package/src/server/index.ts +57 -0
  62. package/src/server/notify.ts +100 -0
  63. package/src/server/rate.ts +47 -0
  64. package/src/server/resend.ts +143 -0
  65. package/src/server/runner.ts +524 -0
  66. package/src/server/secret-links.ts +53 -0
  67. package/src/server/store.ts +150 -0
  68. package/src/server/unsubscribe.ts +78 -0
  69. package/src/server/webhook.ts +55 -0
  70. package/src/supabase/index.ts +210 -0
  71. package/src/web/index.tsx +428 -0
@@ -0,0 +1,174 @@
1
+ /**
2
+ * `@marktiderman/genesis-notifications/runner`: the one outbox runner. Claim a
3
+ * batch of due jobs, send them (grouped, so a destination that takes many at
4
+ * once gets one request), and record what happened. No dependencies.
5
+ *
6
+ * The store is the caller's (a SQL outbox claimed with SKIP LOCKED, a
7
+ * queue): `claim` hands over leased jobs and `record` takes back outcomes.
8
+ * The runner owns the rest: turning each send result into an outcome with a
9
+ * retry time, jittered backoff, the deferral cap, and never throwing for one
10
+ * job's failure. The notifier's push and email channels run on it, and so do
11
+ * genesis-forms' destinations (Sheets, webhooks), each with its own claim.
12
+ *
13
+ * Outcomes:
14
+ * - `sent`: delivered (or already there, on a redelivery: `alreadyThere`).
15
+ * - `filtered`: the sender decided it doesn't apply; nothing sent, nothing to retry.
16
+ * - `skipped`: deliberately not sent (`notSent`: a reason safe to store); never retried.
17
+ * - `retry`: a retryable failure. With the service's own Retry-After it is
18
+ * deferred without counting an attempt (`counted: false`), until
19
+ * `maxDeferrals`; otherwise it backs off by attempt (`counted: true`), until
20
+ * `maxAttempts`.
21
+ * - `failed`: permanent, or out of attempts. With `disable: true` the receiver
22
+ * asked to be switched off (a 410 Gone): stop sending to it.
23
+ *
24
+ * It never logs content, addresses or secrets: only what `label` gives and
25
+ * the senders' safe error messages.
26
+ *
27
+ * @packageDocumentation
28
+ */
29
+ /** One claimed job. Extend it with what your sender needs. */
30
+ interface RunnerJob {
31
+ /** Stable across retries (an idempotency key is made from it). */
32
+ id: string;
33
+ /** This attempt's number, from 1. */
34
+ attempt: number;
35
+ /** Deferrals so far (outcomes with `counted: false`); pass it back so `maxDeferrals` holds. Default 0. */
36
+ deferrals?: number | undefined;
37
+ /** An earlier attempt may have gone through (its lease ran out): senders that can look first do. */
38
+ redelivery?: boolean | undefined;
39
+ }
40
+ /** What a sender says about one job. */
41
+ type JobResult = {
42
+ ok: true;
43
+ /** Deliberately sent nothing; the reason, safe to store. Recorded as `skipped`. */
44
+ notSent?: string | undefined;
45
+ /** It was already there (a redelivery), so nothing was sent this time. */
46
+ alreadyThere?: boolean | undefined;
47
+ } | JobFailure;
48
+ interface JobFailure {
49
+ ok: false;
50
+ /** The same request may succeed later (rate limited, the service down, a timeout). */
51
+ retryable: boolean;
52
+ /** The service said when to come back: retry after this many seconds, without counting an attempt. */
53
+ retryAfterSeconds?: number | undefined;
54
+ status?: number | undefined;
55
+ /** The receiver asked to be switched off. Always with `retryable: false`. */
56
+ disable?: true | undefined;
57
+ /** Safe to store and log: never secrets, content or addresses. */
58
+ error: string;
59
+ }
60
+ type JobOutcome = {
61
+ status: "sent";
62
+ alreadyThere?: boolean;
63
+ } | {
64
+ status: "filtered";
65
+ } | {
66
+ status: "skipped";
67
+ reason: string;
68
+ } | {
69
+ status: "retry";
70
+ error: string;
71
+ retryAt: Date;
72
+ retryAfterSeconds: number;
73
+ /** Whether this attempt counts toward `maxAttempts`. False when the service said when to come back. */
74
+ counted: boolean;
75
+ httpStatus?: number;
76
+ } | {
77
+ status: "failed";
78
+ error: string;
79
+ httpStatus?: number;
80
+ disable?: true;
81
+ };
82
+ interface RunReport {
83
+ claimed: number;
84
+ sent: number;
85
+ filtered: number;
86
+ skipped: number;
87
+ /** Retryable failures, backed off by attempt. */
88
+ retrying: number;
89
+ /** Retryable failures the service said when to retry; no attempt used. */
90
+ deferred: number;
91
+ failed: number;
92
+ /** Recorded after the claim was lost (`record` resolved false). */
93
+ stale: number;
94
+ /** `record` threw. */
95
+ unrecorded: number;
96
+ }
97
+ interface ClaimRequest<Filter> {
98
+ limit: number;
99
+ filter?: Filter | undefined;
100
+ }
101
+ interface SendContext {
102
+ signal?: AbortSignal | undefined;
103
+ }
104
+ interface RunnerOptions<J extends RunnerJob, Filter = undefined> {
105
+ /** Claims up to `limit` due jobs, each leased so no other runner sends it meanwhile. */
106
+ claim(request: ClaimRequest<Filter>): Promise<readonly J[]>;
107
+ /**
108
+ * Sends one group of jobs (those with the same `group` key) and resolves one
109
+ * result per job, in order, or `"filtered"`. A throw fails the whole group
110
+ * with a retryable error.
111
+ */
112
+ send(group: readonly J[], context: SendContext): Promise<readonly (JobResult | "filtered")[]>;
113
+ /** Jobs with the same key are sent together. Default: each job alone. */
114
+ group?: ((job: J) => string) | undefined;
115
+ /**
116
+ * Records an outcome. Resolve `false` when the claim was no longer held: it
117
+ * is counted as stale. A rejection is counted as unrecorded; the lease runs
118
+ * out and the job comes back on its own.
119
+ */
120
+ record(job: J, outcome: JobOutcome): Promise<void | boolean>;
121
+ now?: (() => number) | undefined;
122
+ /** Safe lines (ids, kinds, short reasons). Default: nothing is logged. */
123
+ log?: ((message: string) => void) | undefined;
124
+ /** How a job is named in a log line, capitalised ("Delivery d1 (email)"). Default `Job <id>`. */
125
+ label?: ((job: J) => string) | undefined;
126
+ /** How a job is named when recording it failed ("delivery d1"). Default `job <id>`. */
127
+ recordLabel?: ((job: J) => string) | undefined;
128
+ /** Jobs claimed per run. Default 25. */
129
+ batchSize?: number | undefined;
130
+ /** Counted attempts before a retryable failure is given up. Default 8. */
131
+ maxAttempts?: number | undefined;
132
+ /** Deferrals allowed before a further one counts as an attempt. Default 24. */
133
+ maxDeferrals?: number | undefined;
134
+ /** Seconds to wait after a counted failed attempt. Default `defaultBackoff`. */
135
+ backoff?: ((attempt: number) => number) | undefined;
136
+ }
137
+ interface RunOptions<Filter> {
138
+ filter?: Filter | undefined;
139
+ /** Overrides `batchSize` for this run. */
140
+ limit?: number | undefined;
141
+ /** Aborts the sends in flight. */
142
+ signal?: AbortSignal | undefined;
143
+ }
144
+ interface Runner<Filter = undefined> {
145
+ /** One claim, its sends and their records. Never rejects for a job's failure; a failing `claim` rejects. */
146
+ run(options?: RunOptions<Filter>): Promise<RunReport>;
147
+ }
148
+ declare const DEFAULT_MAX_ATTEMPTS = 8;
149
+ declare const DEFAULT_MAX_DEFERRALS = 24;
150
+ declare const DEFAULT_BATCH_SIZE = 25;
151
+ /**
152
+ * 5 minutes, quadrupling each attempt, at most 24 hours: 5m, 20m, 80m, 5h20m,
153
+ * 21h20m, then 24h; about 3 days across 8 attempts. Each wait is jittered by
154
+ * up to ±20%, so jobs that failed together don't all come back at once.
155
+ */
156
+ declare function defaultBackoff(attempt: number, random?: () => number): number;
157
+ /** Turns a send result into an outcome for a job on its `attempt`. */
158
+ declare function jobOutcome(result: JobResult, attempt: number, options?: {
159
+ now?: number | undefined;
160
+ maxAttempts?: number | undefined;
161
+ backoff?: ((attempt: number) => number) | undefined;
162
+ deferrals?: number | undefined;
163
+ maxDeferrals?: number | undefined;
164
+ }): JobOutcome;
165
+ /** A thrown error as a retryable failure with a safe message. */
166
+ declare function failureOf(error: unknown): JobFailure;
167
+ /**
168
+ * The runner.
169
+ *
170
+ * @stability Beta
171
+ */
172
+ declare function createRunner<J extends RunnerJob, Filter = undefined>(options: RunnerOptions<J, Filter>): Runner<Filter>;
173
+
174
+ export { type ClaimRequest, DEFAULT_BATCH_SIZE, DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_DEFERRALS, type JobFailure, type JobOutcome, type JobResult, type RunOptions, type RunReport, type Runner, type RunnerJob, type RunnerOptions, type SendContext, createRunner, defaultBackoff, failureOf, jobOutcome };
package/dist/runner.js ADDED
@@ -0,0 +1,3 @@
1
+ export { DEFAULT_BATCH_SIZE, DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_DEFERRALS, createRunner, defaultBackoff, failureOf, jobOutcome } from './chunk-UXNWI7OS.js';
2
+ //# sourceMappingURL=runner.js.map
3
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"runner.js"}