@marktiderman/genesis-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.
- package/LICENSE +21 -0
- package/README.md +251 -2
- package/dist/blocks.d.ts +10 -0
- package/dist/blocks.js +35 -0
- package/dist/blocks.js.map +1 -0
- package/dist/chunk-2BG336II.js +124 -0
- package/dist/chunk-2BG336II.js.map +1 -0
- package/dist/chunk-7PHA3WVG.js +51 -0
- package/dist/chunk-7PHA3WVG.js.map +1 -0
- package/dist/chunk-EWZB35V6.js +113 -0
- package/dist/chunk-EWZB35V6.js.map +1 -0
- package/dist/chunk-UXNWI7OS.js +118 -0
- package/dist/chunk-UXNWI7OS.js.map +1 -0
- package/dist/email-render-CID_pXDy.d.ts +53 -0
- package/dist/email.d.ts +46 -0
- package/dist/email.js +64 -0
- package/dist/email.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +92 -0
- package/dist/native.js +170 -0
- package/dist/native.js.map +1 -0
- package/dist/react.d.ts +98 -0
- package/dist/react.js +4 -0
- package/dist/react.js.map +1 -0
- package/dist/runner.d.ts +174 -0
- package/dist/runner.js +3 -0
- package/dist/runner.js.map +1 -0
- package/dist/server.d.ts +658 -0
- package/dist/server.js +995 -0
- package/dist/server.js.map +1 -0
- package/dist/supabase.d.ts +118 -0
- package/dist/supabase.js +3 -0
- package/dist/supabase.js.map +1 -0
- package/dist/types-BilguCPW.d.ts +332 -0
- package/dist/web.d.ts +79 -0
- package/dist/web.js +233 -0
- package/dist/web.js.map +1 -0
- package/functions/genesis-notifications/index.ts +60 -0
- package/functions/genesis-notifications/notifications.ts +18 -0
- package/migrations/0001_genesis_notifications.sql +2057 -0
- package/migrations/tests/0001_genesis_notifications.test.sql +473 -0
- package/package.json +191 -3
- package/src/blocks.ts +42 -0
- package/src/core/index.ts +43 -0
- package/src/core/registry.ts +354 -0
- package/src/core/types.ts +90 -0
- package/src/core/validate.ts +73 -0
- package/src/email/index.tsx +142 -0
- package/src/index.ts +6 -0
- package/src/native/index.tsx +283 -0
- package/src/react/index.tsx +226 -0
- package/src/runner/index.ts +299 -0
- package/src/server/crypto.ts +44 -0
- package/src/server/dns.ts +106 -0
- package/src/server/email-render.ts +90 -0
- package/src/server/errors.ts +61 -0
- package/src/server/expo.ts +133 -0
- package/src/server/handler.ts +127 -0
- package/src/server/index.ts +57 -0
- package/src/server/notify.ts +100 -0
- package/src/server/rate.ts +47 -0
- package/src/server/resend.ts +143 -0
- package/src/server/runner.ts +524 -0
- package/src/server/secret-links.ts +53 -0
- package/src/server/store.ts +150 -0
- package/src/server/unsubscribe.ts +78 -0
- package/src/server/webhook.ts +55 -0
- package/src/supabase/index.ts +210 -0
- package/src/web/index.tsx +428 -0
package/dist/runner.d.ts
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"runner.js"}
|