easy-ping 0.3.0 → 0.5.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/README.md +174 -82
- package/dist/adapter-B7R4rXtJ.d.cts +17 -0
- package/dist/adapter-B7R4rXtJ.d.ts +17 -0
- package/dist/{adapter-BfLe2J0t.d.cts → adapter-D3lTy-4E.d.cts} +6 -32
- package/dist/{adapter-BfLe2J0t.d.ts → adapter-D3lTy-4E.d.ts} +6 -32
- package/dist/adapters/drizzle.cjs +4 -4
- package/dist/adapters/drizzle.cjs.map +1 -1
- package/dist/adapters/drizzle.d.cts +35 -1
- package/dist/adapters/drizzle.d.ts +35 -1
- package/dist/adapters/drizzle.js +5 -5
- package/dist/adapters/drizzle.js.map +1 -1
- package/dist/adapters/mongodb.cjs +8 -7
- package/dist/adapters/mongodb.cjs.map +1 -1
- package/dist/adapters/mongodb.d.cts +4 -17
- package/dist/adapters/mongodb.d.ts +4 -17
- package/dist/adapters/mongodb.js +7 -7
- package/dist/adapters/mongodb.js.map +1 -1
- package/dist/adapters/mysql.cjs +605 -0
- package/dist/adapters/mysql.cjs.map +1 -0
- package/dist/adapters/mysql.d.cts +38 -0
- package/dist/adapters/mysql.d.ts +38 -0
- package/dist/adapters/mysql.js +51 -0
- package/dist/adapters/mysql.js.map +1 -0
- package/dist/adapters/postgres.cjs +19 -11
- package/dist/adapters/postgres.cjs.map +1 -1
- package/dist/adapters/postgres.d.cts +9 -54
- package/dist/adapters/postgres.d.ts +9 -54
- package/dist/adapters/postgres.js +6 -41
- package/dist/adapters/postgres.js.map +1 -1
- package/dist/adapters/sqlite.cjs +593 -0
- package/dist/adapters/sqlite.cjs.map +1 -0
- package/dist/adapters/sqlite.d.cts +37 -0
- package/dist/adapters/sqlite.d.ts +37 -0
- package/dist/adapters/sqlite.js +53 -0
- package/dist/adapters/sqlite.js.map +1 -0
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.cts +2 -14
- package/dist/browser.d.ts +2 -14
- package/dist/browser.js.map +1 -1
- package/dist/chunk-2EYKPF5A.js +393 -0
- package/dist/chunk-2EYKPF5A.js.map +1 -0
- package/dist/{chunk-OZSOIX23.js → chunk-3VOQOGDR.js} +54 -12
- package/dist/chunk-3VOQOGDR.js.map +1 -0
- package/dist/{chunk-NYDVNMKG.js → chunk-AEYYTE5I.js} +4 -3
- package/dist/chunk-AEYYTE5I.js.map +1 -0
- package/dist/chunk-BECMTNK7.js +315 -0
- package/dist/chunk-BECMTNK7.js.map +1 -0
- package/dist/chunk-CK5UPVYP.js +48 -0
- package/dist/chunk-CK5UPVYP.js.map +1 -0
- package/dist/{chunk-BV3U542P.js → chunk-E4IZ2CQC.js} +2 -2
- package/dist/chunk-E4IZ2CQC.js.map +1 -0
- package/dist/chunk-GJNLOQJS.js +226 -0
- package/dist/chunk-GJNLOQJS.js.map +1 -0
- package/dist/chunk-HY47TQMY.js +52 -0
- package/dist/chunk-HY47TQMY.js.map +1 -0
- package/dist/chunk-M4R77BJD.js +64 -0
- package/dist/chunk-M4R77BJD.js.map +1 -0
- package/dist/{chunk-RBDW4S6F.js → chunk-XIDVLYNQ.js} +14 -4
- package/dist/chunk-XIDVLYNQ.js.map +1 -0
- package/dist/client.cjs.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +362 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -37
- package/dist/index.d.ts +56 -37
- package/dist/index.js +120 -359
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +27 -6
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +8 -2
- package/dist/node.d.ts +8 -2
- package/dist/node.js +26 -6
- package/dist/node.js.map +1 -1
- package/dist/{plugin-CmPOFVmn.d.ts → plugin-CHd2ZiGC.d.cts} +37 -25
- package/dist/{plugin-f5KaFWSN.d.cts → plugin-Cg9usuUi.d.ts} +37 -25
- package/dist/plugins/digests.cjs +1 -4
- package/dist/plugins/digests.cjs.map +1 -1
- package/dist/plugins/digests.d.cts +6 -31
- package/dist/plugins/digests.d.ts +6 -31
- package/dist/plugins/digests.js +1 -4
- package/dist/plugins/digests.js.map +1 -1
- package/dist/plugins/preferences-client.cjs.map +1 -1
- package/dist/plugins/preferences-client.d.cts +2 -2
- package/dist/plugins/preferences-client.d.ts +2 -2
- package/dist/plugins/preferences-client.js +1 -1
- package/dist/plugins/preferences-react.cjs.map +1 -1
- package/dist/plugins/preferences-react.js.map +1 -1
- package/dist/plugins/preferences.cjs +101 -17
- package/dist/plugins/preferences.cjs.map +1 -1
- package/dist/plugins/preferences.d.cts +4 -2
- package/dist/plugins/preferences.d.ts +4 -2
- package/dist/plugins/preferences.js +36 -9
- package/dist/plugins/preferences.js.map +1 -1
- package/dist/plugins/push.cjs +198 -40
- package/dist/plugins/push.cjs.map +1 -1
- package/dist/plugins/push.d.cts +20 -18
- package/dist/plugins/push.d.ts +20 -18
- package/dist/plugins/push.js +141 -41
- package/dist/plugins/push.js.map +1 -1
- package/dist/providers/web-push.cjs +37 -11
- package/dist/providers/web-push.cjs.map +1 -1
- package/dist/providers/web-push.d.cts +10 -11
- package/dist/providers/web-push.d.ts +10 -11
- package/dist/providers/web-push.js +37 -12
- package/dist/providers/web-push.js.map +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -4
- package/dist/react.d.ts +1 -4
- package/dist/react.js.map +1 -1
- package/dist/schema.cjs +121 -2
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +25 -14
- package/dist/schema.d.ts +25 -14
- package/dist/schema.js +4 -2
- package/dist/testing.cjs +3 -19
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +4 -22
- package/dist/testing.d.ts +4 -22
- package/dist/testing.js +3 -19
- package/dist/testing.js.map +1 -1
- package/package.json +12 -1
- package/dist/chunk-BV3U542P.js.map +0 -1
- package/dist/chunk-NYDVNMKG.js.map +0 -1
- package/dist/chunk-OZSOIX23.js.map +0 -1
- package/dist/chunk-RBDW4S6F.js.map +0 -1
- package/dist/chunk-W3BMEPPQ.js +0 -50
- package/dist/chunk-W3BMEPPQ.js.map +0 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// src/core/backoff.ts
|
|
2
|
+
var SCHEDULE_MS = [3e4, 12e4, 48e4, 192e4];
|
|
3
|
+
function backoffMs(attempt, strategy = "exponential") {
|
|
4
|
+
if (typeof strategy === "function") return Math.max(0, strategy(attempt));
|
|
5
|
+
const index = Math.min(Math.max(attempt, 1), SCHEDULE_MS.length) - 1;
|
|
6
|
+
const base = SCHEDULE_MS[index] ?? SCHEDULE_MS[SCHEDULE_MS.length - 1] ?? 3e4;
|
|
7
|
+
return Math.round(base * (0.8 + Math.random() * 0.4));
|
|
8
|
+
}
|
|
9
|
+
var nextAttemptAt = (attempt, strategy, now = /* @__PURE__ */ new Date()) => new Date(now.getTime() + backoffMs(attempt, strategy));
|
|
10
|
+
|
|
11
|
+
// src/core/runner.ts
|
|
12
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
async function withTimeout(run, ms, label) {
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
let timer;
|
|
16
|
+
const timeout = new Promise((_, reject) => {
|
|
17
|
+
timer = setTimeout(() => {
|
|
18
|
+
controller.abort();
|
|
19
|
+
reject(new Error(`${label} timed out after ${ms}ms`));
|
|
20
|
+
}, ms);
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
return await Promise.race([run(controller.signal), timeout]);
|
|
24
|
+
} finally {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
var redactErrorMessage = (text) => text.replace(/[^\s"'<>()[\]{},;]+@[^\s"'<>()[\]{},;]+/g, "[redacted-email]");
|
|
29
|
+
var message = (error) => redactErrorMessage(error instanceof Error ? error.message : String(error));
|
|
30
|
+
function createRunner(deps) {
|
|
31
|
+
async function deliverOne(delivery, recipient) {
|
|
32
|
+
if (!recipient) {
|
|
33
|
+
return { result: "failed", error: "recipient could not be resolved", retryable: false };
|
|
34
|
+
}
|
|
35
|
+
if (delivery.channel === "inApp") {
|
|
36
|
+
return { result: "sent" };
|
|
37
|
+
}
|
|
38
|
+
if (delivery.channel === "email") {
|
|
39
|
+
const provider = deps.channels.email?.provider;
|
|
40
|
+
if (!provider) {
|
|
41
|
+
return { result: "failed", error: "no email provider configured", retryable: false };
|
|
42
|
+
}
|
|
43
|
+
const to = recipient.email;
|
|
44
|
+
if (!to) {
|
|
45
|
+
return { result: "failed", error: "recipient has no email address", retryable: false };
|
|
46
|
+
}
|
|
47
|
+
const email = deps.definitions[delivery.notification.type]?.email;
|
|
48
|
+
if (!email) {
|
|
49
|
+
return {
|
|
50
|
+
result: "failed",
|
|
51
|
+
error: `notification type "${delivery.notification.type}" has no email template`,
|
|
52
|
+
retryable: false
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const payload = delivery.notification.payload;
|
|
57
|
+
const html = await email.template(payload);
|
|
58
|
+
await withTimeout(
|
|
59
|
+
(signal) => provider.send({
|
|
60
|
+
to,
|
|
61
|
+
subject: email.subject(payload),
|
|
62
|
+
html,
|
|
63
|
+
// Delivery is at-least-once; the provider dedupes on this.
|
|
64
|
+
idempotencyKey: delivery.id,
|
|
65
|
+
signal
|
|
66
|
+
}),
|
|
67
|
+
provider.timeoutMs ?? 3e4,
|
|
68
|
+
`${provider.name} send`
|
|
69
|
+
);
|
|
70
|
+
return { result: "sent" };
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return {
|
|
73
|
+
result: "failed",
|
|
74
|
+
error: message(error),
|
|
75
|
+
retryable: provider.isRetryable(error)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const owner = deps.plugins.find(
|
|
80
|
+
(plugin) => plugin.channels?.includes(delivery.channel) && plugin.hooks?.deliver
|
|
81
|
+
);
|
|
82
|
+
if (!owner?.hooks?.deliver) {
|
|
83
|
+
return {
|
|
84
|
+
result: "failed",
|
|
85
|
+
error: `no plugin delivers channel "${delivery.channel}"`,
|
|
86
|
+
retryable: false
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return await withTimeout(
|
|
91
|
+
(signal) => Promise.resolve(
|
|
92
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded directly above
|
|
93
|
+
owner.hooks.deliver({
|
|
94
|
+
channel: delivery.channel,
|
|
95
|
+
deliveryId: delivery.id,
|
|
96
|
+
attempt: delivery.attempts + 1,
|
|
97
|
+
recipient,
|
|
98
|
+
notification: {
|
|
99
|
+
id: delivery.notificationId,
|
|
100
|
+
userId: delivery.notification.userId,
|
|
101
|
+
type: delivery.notification.type,
|
|
102
|
+
payload: delivery.notification.payload
|
|
103
|
+
},
|
|
104
|
+
signal
|
|
105
|
+
})
|
|
106
|
+
),
|
|
107
|
+
3e4,
|
|
108
|
+
`plugin "${owner.id}" deliver`
|
|
109
|
+
);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return { result: "failed", error: message(error), retryable: true };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function notifyAfterDeliver(delivery, outcome) {
|
|
115
|
+
const isTerminal = outcome.result !== "failed" || !outcome.retryable || delivery.attempts + 1 >= delivery.maxAttempts;
|
|
116
|
+
for (const plugin of deps.plugins) {
|
|
117
|
+
const hook = plugin.hooks?.afterDeliver;
|
|
118
|
+
if (!hook) continue;
|
|
119
|
+
try {
|
|
120
|
+
await hook({
|
|
121
|
+
deliveryId: delivery.id,
|
|
122
|
+
notificationId: delivery.notificationId,
|
|
123
|
+
userId: delivery.notification.userId,
|
|
124
|
+
channel: delivery.channel,
|
|
125
|
+
outcome,
|
|
126
|
+
attempt: delivery.attempts + 1,
|
|
127
|
+
isTerminal
|
|
128
|
+
});
|
|
129
|
+
} catch (error) {
|
|
130
|
+
deps.logger.error(`plugin "${plugin.id}" afterDeliver threw; ignoring`, {
|
|
131
|
+
deliveryId: delivery.id,
|
|
132
|
+
error
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function runOnce(options = {}) {
|
|
138
|
+
const claimToken = crypto.randomUUID();
|
|
139
|
+
const claimed = await deps.adapter.claimPendingDeliveries({
|
|
140
|
+
limit: options.limit ?? deps.batchSize,
|
|
141
|
+
leaseMs: deps.leaseMs,
|
|
142
|
+
claimToken,
|
|
143
|
+
...options.channels ? { channels: options.channels } : {},
|
|
144
|
+
...options.ids ? { ids: options.ids } : {}
|
|
145
|
+
});
|
|
146
|
+
if (claimed.length === 0) return { claimed: 0, sent: 0, failed: 0, skipped: 0 };
|
|
147
|
+
const userIds = [...new Set(claimed.map((row) => row.notification.userId))];
|
|
148
|
+
const recipients = await deps.getRecipients(userIds);
|
|
149
|
+
const byUserId = new Map(recipients.map((recipient) => [recipient.userId, recipient]));
|
|
150
|
+
const releases = [];
|
|
151
|
+
let sent = 0;
|
|
152
|
+
let failed = 0;
|
|
153
|
+
let skipped = 0;
|
|
154
|
+
for (const delivery of claimed) {
|
|
155
|
+
const outcome = await deliverOne(delivery, byUserId.get(delivery.notification.userId));
|
|
156
|
+
if (outcome.result === "sent") sent += 1;
|
|
157
|
+
else if (outcome.result === "skipped") skipped += 1;
|
|
158
|
+
else failed += 1;
|
|
159
|
+
releases.push({
|
|
160
|
+
id: delivery.id,
|
|
161
|
+
claimToken,
|
|
162
|
+
outcome,
|
|
163
|
+
...outcome.result === "failed" && outcome.retryable ? { nextAttemptAt: nextAttemptAt(delivery.attempts + 1, deps.backoff) } : {}
|
|
164
|
+
});
|
|
165
|
+
await notifyAfterDeliver(delivery, outcome);
|
|
166
|
+
}
|
|
167
|
+
await deps.adapter.releaseDeliveries(releases);
|
|
168
|
+
return { claimed: claimed.length, sent, failed, skipped };
|
|
169
|
+
}
|
|
170
|
+
async function drain(options = {}) {
|
|
171
|
+
const maxSweeps = options.maxSweeps ?? 50;
|
|
172
|
+
const total = { claimed: 0, sent: 0, failed: 0, skipped: 0 };
|
|
173
|
+
for (let sweep = 0; sweep < maxSweeps; sweep += 1) {
|
|
174
|
+
const result = await runOnce(options.ids ? { ids: options.ids } : {});
|
|
175
|
+
total.claimed += result.claimed;
|
|
176
|
+
total.sent += result.sent;
|
|
177
|
+
total.failed += result.failed;
|
|
178
|
+
total.skipped += result.skipped;
|
|
179
|
+
if (result.claimed === 0) break;
|
|
180
|
+
}
|
|
181
|
+
return total;
|
|
182
|
+
}
|
|
183
|
+
let warnedMissingWaitUntil = false;
|
|
184
|
+
function scheduleDeferred(waitUntil, ids) {
|
|
185
|
+
if (waitUntil) {
|
|
186
|
+
waitUntil(
|
|
187
|
+
drain({ maxSweeps: 10, ...ids ? { ids } : {} }).catch(
|
|
188
|
+
(error) => deps.logger.error("deferred sweep failed", { error })
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (!warnedMissingWaitUntil) {
|
|
194
|
+
warnedMissingWaitUntil = true;
|
|
195
|
+
deps.logger.warn(
|
|
196
|
+
"delivery.mode is 'deferred' but no delivery.waitUntil was supplied \u2014 falling back to the cron sweep. Notifications are not lost, but they will not be delivered until POST /cron runs."
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function startWorker(options = {}) {
|
|
201
|
+
const intervalMs = options.intervalMs ?? 1e3;
|
|
202
|
+
let stopped = false;
|
|
203
|
+
const loop = (async () => {
|
|
204
|
+
while (!stopped) {
|
|
205
|
+
try {
|
|
206
|
+
const result = await runOnce({ limit: options.batchSize ?? deps.batchSize });
|
|
207
|
+
if (result.claimed === 0) await sleep(intervalMs);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
deps.logger.error("worker sweep failed", { error });
|
|
210
|
+
await sleep(intervalMs);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
})();
|
|
214
|
+
return {
|
|
215
|
+
stop: async () => {
|
|
216
|
+
stopped = true;
|
|
217
|
+
await loop;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return { runOnce, drain, scheduleDeferred, startWorker };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export { createRunner, redactErrorMessage };
|
|
225
|
+
//# sourceMappingURL=chunk-GJNLOQJS.js.map
|
|
226
|
+
//# sourceMappingURL=chunk-GJNLOQJS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/backoff.ts","../src/core/runner.ts"],"names":[],"mappings":"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","file":"chunk-GJNLOQJS.js","sourcesContent":["/** Exponential backoff with ±20% jitter, floored by the sweep interval. RFC 0001 §4. */\nconst SCHEDULE_MS = [30_000, 120_000, 480_000, 1_920_000] as const;\n\nexport type Backoff = \"exponential\" | ((attempt: number) => number);\n\nexport function backoffMs(attempt: number, strategy: Backoff = \"exponential\"): number {\n if (typeof strategy === \"function\") return Math.max(0, strategy(attempt));\n\n const index = Math.min(Math.max(attempt, 1), SCHEDULE_MS.length) - 1;\n const base = SCHEDULE_MS[index] ?? SCHEDULE_MS[SCHEDULE_MS.length - 1] ?? 30_000;\n return Math.round(base * (0.8 + Math.random() * 0.4));\n}\n\nexport const nextAttemptAt = (attempt: number, strategy?: Backoff, now = new Date()): Date =>\n new Date(now.getTime() + backoffMs(attempt, strategy));\n","import type { ClaimedDelivery, DatabaseAdapter, DeliveryOutcome, DeliveryRelease } from \"./adapter\";\nimport { type Backoff, nextAttemptAt } from \"./backoff\";\nimport type { ChannelsConfig, Worker } from \"./config\";\nimport type { NotificationDefinitions } from \"./definition\";\nimport type { Logger } from \"./errors\";\nimport type { AnyPlugin } from \"./plugin\";\nimport type { Channel, Recipient } from \"./types\";\n\nexport type RunnerDeps = {\n adapter: DatabaseAdapter;\n definitions: NotificationDefinitions;\n channels: ChannelsConfig;\n plugins: readonly AnyPlugin[];\n getRecipients: (userIds: readonly string[]) => Promise<readonly Recipient[]>;\n logger: Logger;\n leaseMs: number;\n batchSize: number;\n backoff: Backoff;\n};\n\nexport type SweepResult = {\n claimed: number;\n sent: number;\n failed: number;\n /** Deliveries that had nothing to do. Not errors; see DeliveryOutcome. */\n skipped: number;\n};\n\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nasync function withTimeout<T>(\n run: (signal: AbortSignal) => Promise<T>,\n ms: number,\n label: string,\n): Promise<T> {\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n // Abort first: racing a promise leaves the original request running.\n controller.abort();\n reject(new Error(`${label} timed out after ${ms}ms`));\n }, ms);\n });\n\n try {\n return await Promise.race([run(controller.signal), timeout]);\n } finally {\n clearTimeout(timer);\n }\n}\n\n/** Persisted to `last_error`, which admin tooling reads: addresses do not go in. */\nexport const redactErrorMessage = (text: string) =>\n text.replace(/[^\\s\"'<>()[\\]{},;]+@[^\\s\"'<>()[\\]{},;]+/g, \"[redacted-email]\");\n\nconst message = (error: unknown) =>\n redactErrorMessage(error instanceof Error ? error.message : String(error));\n\nexport function createRunner(deps: RunnerDeps) {\n async function deliverOne(\n delivery: ClaimedDelivery,\n recipient: Recipient | undefined,\n ): Promise<DeliveryOutcome> {\n if (!recipient) {\n return { result: \"failed\", error: \"recipient could not be resolved\", retryable: false };\n }\n\n if (delivery.channel === \"inApp\") {\n // The notification row is the delivery. Nothing to transmit.\n return { result: \"sent\" };\n }\n\n if (delivery.channel === \"email\") {\n const provider = deps.channels.email?.provider;\n if (!provider) {\n return { result: \"failed\", error: \"no email provider configured\", retryable: false };\n }\n // Captured before the closure below: narrowing does not survive into it.\n const to = recipient.email;\n if (!to) {\n return { result: \"failed\", error: \"recipient has no email address\", retryable: false };\n }\n\n const email = deps.definitions[delivery.notification.type]?.email;\n if (!email) {\n return {\n result: \"failed\",\n error: `notification type \"${delivery.notification.type}\" has no email template`,\n retryable: false,\n };\n }\n\n try {\n const payload = delivery.notification.payload as never;\n const html = await email.template(payload);\n\n await withTimeout(\n (signal) =>\n provider.send({\n to,\n subject: email.subject(payload),\n html,\n // Delivery is at-least-once; the provider dedupes on this.\n idempotencyKey: delivery.id,\n signal,\n }),\n provider.timeoutMs ?? 30_000,\n `${provider.name} send`,\n );\n\n return { result: \"sent\" };\n } catch (error) {\n return {\n result: \"failed\",\n error: message(error),\n retryable: provider.isRetryable(error),\n };\n }\n }\n\n const owner = deps.plugins.find(\n (plugin) => plugin.channels?.includes(delivery.channel) && plugin.hooks?.deliver,\n );\n if (!owner?.hooks?.deliver) {\n return {\n result: \"failed\",\n error: `no plugin delivers channel \"${delivery.channel}\"`,\n retryable: false,\n };\n }\n\n try {\n return await withTimeout(\n (signal) =>\n Promise.resolve(\n // biome-ignore lint/style/noNonNullAssertion: guarded directly above\n owner.hooks!.deliver!({\n channel: delivery.channel,\n deliveryId: delivery.id,\n attempt: delivery.attempts + 1,\n recipient,\n notification: {\n id: delivery.notificationId,\n userId: delivery.notification.userId,\n type: delivery.notification.type,\n payload: delivery.notification.payload,\n },\n signal,\n }),\n ),\n 30_000,\n `plugin \"${owner.id}\" deliver`,\n );\n } catch (error) {\n return { result: \"failed\", error: message(error), retryable: true };\n }\n }\n\n async function notifyAfterDeliver(delivery: ClaimedDelivery, outcome: DeliveryOutcome) {\n const isTerminal =\n outcome.result !== \"failed\" ||\n !outcome.retryable ||\n delivery.attempts + 1 >= delivery.maxAttempts;\n\n for (const plugin of deps.plugins) {\n const hook = plugin.hooks?.afterDeliver;\n if (!hook) continue;\n try {\n await hook({\n deliveryId: delivery.id,\n notificationId: delivery.notificationId,\n userId: delivery.notification.userId,\n channel: delivery.channel,\n outcome,\n attempt: delivery.attempts + 1,\n isTerminal,\n });\n } catch (error) {\n // Fail open: the message is already sent. RFC 0004 §5.\n deps.logger.error(`plugin \"${plugin.id}\" afterDeliver threw; ignoring`, {\n deliveryId: delivery.id,\n error,\n });\n }\n }\n }\n\n async function runOnce(\n options: { limit?: number; channels?: readonly Channel[]; ids?: readonly string[] } = {},\n ): Promise<SweepResult> {\n const claimToken = crypto.randomUUID();\n\n const claimed = await deps.adapter.claimPendingDeliveries({\n limit: options.limit ?? deps.batchSize,\n leaseMs: deps.leaseMs,\n claimToken,\n ...(options.channels ? { channels: options.channels } : {}),\n ...(options.ids ? { ids: options.ids } : {}),\n });\n\n if (claimed.length === 0) return { claimed: 0, sent: 0, failed: 0, skipped: 0 };\n\n const userIds = [...new Set(claimed.map((row) => row.notification.userId))];\n const recipients = await deps.getRecipients(userIds);\n const byUserId = new Map(recipients.map((recipient) => [recipient.userId, recipient]));\n\n const releases: DeliveryRelease[] = [];\n let sent = 0;\n let failed = 0;\n let skipped = 0;\n\n for (const delivery of claimed) {\n const outcome = await deliverOne(delivery, byUserId.get(delivery.notification.userId));\n\n if (outcome.result === \"sent\") sent += 1;\n else if (outcome.result === \"skipped\") skipped += 1;\n else failed += 1;\n\n releases.push({\n id: delivery.id,\n claimToken,\n outcome,\n ...(outcome.result === \"failed\" && outcome.retryable\n ? { nextAttemptAt: nextAttemptAt(delivery.attempts + 1, deps.backoff) }\n : {}),\n });\n\n await notifyAfterDeliver(delivery, outcome);\n }\n\n await deps.adapter.releaseDeliveries(releases);\n return { claimed: claimed.length, sent, failed, skipped };\n }\n\n /** Drains until empty. Pass `ids` to bound it to one send. */\n async function drain(\n options: { maxSweeps?: number; ids?: readonly string[] } = {},\n ): Promise<SweepResult> {\n const maxSweeps = options.maxSweeps ?? 50;\n const total: SweepResult = { claimed: 0, sent: 0, failed: 0, skipped: 0 };\n\n for (let sweep = 0; sweep < maxSweeps; sweep += 1) {\n const result = await runOnce(options.ids ? { ids: options.ids } : {});\n total.claimed += result.claimed;\n total.sent += result.sent;\n total.failed += result.failed;\n total.skipped += result.skipped;\n if (result.claimed === 0) break;\n }\n\n return total;\n }\n\n let warnedMissingWaitUntil = false;\n\n function scheduleDeferred(\n waitUntil?: (promise: Promise<unknown>) => void,\n ids?: readonly string[],\n ): void {\n if (waitUntil) {\n waitUntil(\n drain({ maxSweeps: 10, ...(ids ? { ids } : {}) }).catch((error) =>\n deps.logger.error(\"deferred sweep failed\", { error }),\n ),\n );\n return;\n }\n\n if (!warnedMissingWaitUntil) {\n warnedMissingWaitUntil = true;\n deps.logger.warn(\n \"delivery.mode is 'deferred' but no delivery.waitUntil was supplied — \" +\n \"falling back to the cron sweep. Notifications are not lost, but they \" +\n \"will not be delivered until POST /cron runs.\",\n );\n }\n }\n\n function startWorker(options: { intervalMs?: number; batchSize?: number } = {}): Worker {\n const intervalMs = options.intervalMs ?? 1_000;\n let stopped = false;\n\n const loop = (async () => {\n while (!stopped) {\n try {\n const result = await runOnce({ limit: options.batchSize ?? deps.batchSize });\n // Only idle when there was nothing to do; otherwise keep draining.\n if (result.claimed === 0) await sleep(intervalMs);\n } catch (error) {\n deps.logger.error(\"worker sweep failed\", { error });\n await sleep(intervalMs);\n }\n }\n })();\n\n return {\n stop: async () => {\n stopped = true;\n // Let the in-flight sweep finish and release its leases.\n await loop;\n },\n };\n }\n\n return { runOnce, drain, scheduleDeferred, startWorker };\n}\n\nexport type Runner = ReturnType<typeof createRunner>;\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { toSnakeCase } from './chunk-AEYYTE5I.js';
|
|
2
|
+
|
|
3
|
+
// src/schema/render-sqlite.ts
|
|
4
|
+
var SQL_TYPE = {
|
|
5
|
+
string: "TEXT",
|
|
6
|
+
number: "INTEGER",
|
|
7
|
+
boolean: "INTEGER",
|
|
8
|
+
// ISO-8601 with milliseconds and a trailing Z, the same 24 characters
|
|
9
|
+
// `Date#toISOString` writes, so `<=` on the text is `<=` on the instant.
|
|
10
|
+
date: "TEXT",
|
|
11
|
+
json: "TEXT"
|
|
12
|
+
};
|
|
13
|
+
var quote = (value) => `"${value.split('"').join('""')}"`;
|
|
14
|
+
var defaultClause = (spec) => {
|
|
15
|
+
if (spec.defaultNow) return " DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))";
|
|
16
|
+
if (spec.default === void 0) return "";
|
|
17
|
+
if (typeof spec.default === "string") return ` DEFAULT '${spec.default.replace(/'/g, "''")}'`;
|
|
18
|
+
if (typeof spec.default === "boolean") return ` DEFAULT ${spec.default ? 1 : 0}`;
|
|
19
|
+
return ` DEFAULT ${spec.default}`;
|
|
20
|
+
};
|
|
21
|
+
function renderSqliteDdl(schema, prefix = "") {
|
|
22
|
+
const statements = [];
|
|
23
|
+
for (const table of Object.values(schema)) {
|
|
24
|
+
const name = quote(prefix + table.tableName);
|
|
25
|
+
const columns = Object.entries(table.fields).map(([field, spec]) => {
|
|
26
|
+
const nullability = spec.required ? " NOT NULL" : "";
|
|
27
|
+
return ` ${quote(toSnakeCase(field))} ${SQL_TYPE[spec.type]}${nullability}${defaultClause(spec)}`;
|
|
28
|
+
});
|
|
29
|
+
if (table.primaryKey?.length) {
|
|
30
|
+
columns.push(
|
|
31
|
+
` PRIMARY KEY (${table.primaryKey.map((f) => quote(toSnakeCase(f))).join(", ")})`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
statements.push(`CREATE TABLE IF NOT EXISTS ${name} (
|
|
35
|
+
${columns.join(",\n")}
|
|
36
|
+
)`);
|
|
37
|
+
for (const index of table.indexes ?? []) {
|
|
38
|
+
const indexName = quote(
|
|
39
|
+
prefix + (index.name ?? `${table.tableName}_${index.on.join("_")}_idx`)
|
|
40
|
+
);
|
|
41
|
+
const cols = index.on.map((f) => quote(toSnakeCase(f))).join(", ");
|
|
42
|
+
statements.push(
|
|
43
|
+
`CREATE ${index.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${indexName} ON ${name} (${cols})`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return statements;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { renderSqliteDdl };
|
|
51
|
+
//# sourceMappingURL=chunk-HY47TQMY.js.map
|
|
52
|
+
//# sourceMappingURL=chunk-HY47TQMY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema/render-sqlite.ts"],"names":[],"mappings":"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","file":"chunk-HY47TQMY.js","sourcesContent":["import type { FieldDeclaration, SchemaDeclaration } from \"../core/plugin\";\nimport { toSnakeCase } from \"./declaration\";\n\nconst SQL_TYPE: Record<FieldDeclaration[\"type\"], string> = {\n string: \"TEXT\",\n number: \"INTEGER\",\n boolean: \"INTEGER\",\n // ISO-8601 with milliseconds and a trailing Z, the same 24 characters\n // `Date#toISOString` writes, so `<=` on the text is `<=` on the instant.\n date: \"TEXT\",\n json: \"TEXT\",\n};\n\nconst quote = (value: string) => `\"${value.split('\"').join('\"\"')}\"`;\n\nconst defaultClause = (spec: FieldDeclaration): string => {\n if (spec.defaultNow) return \" DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))\";\n if (spec.default === undefined) return \"\";\n if (typeof spec.default === \"string\") return ` DEFAULT '${spec.default.replace(/'/g, \"''\")}'`;\n if (typeof spec.default === \"boolean\") return ` DEFAULT ${spec.default ? 1 : 0}`;\n return ` DEFAULT ${spec.default}`;\n};\n\n/** SchemaDeclaration to SQLite DDL. Every statement is `IF NOT EXISTS`. */\nexport function renderSqliteDdl(schema: SchemaDeclaration, prefix = \"\"): string[] {\n const statements: string[] = [];\n\n for (const table of Object.values(schema)) {\n const name = quote(prefix + table.tableName);\n\n const columns = Object.entries(table.fields).map(([field, spec]) => {\n const nullability = spec.required ? \" NOT NULL\" : \"\";\n return ` ${quote(toSnakeCase(field))} ${SQL_TYPE[spec.type]}${nullability}${defaultClause(spec)}`;\n });\n\n if (table.primaryKey?.length) {\n columns.push(\n ` PRIMARY KEY (${table.primaryKey.map((f) => quote(toSnakeCase(f))).join(\", \")})`,\n );\n }\n\n statements.push(`CREATE TABLE IF NOT EXISTS ${name} (\\n${columns.join(\",\\n\")}\\n)`);\n\n for (const index of table.indexes ?? []) {\n const indexName = quote(\n prefix + (index.name ?? `${table.tableName}_${index.on.join(\"_\")}_idx`),\n );\n const cols = index.on.map((f) => quote(toSnakeCase(f))).join(\", \");\n statements.push(\n `CREATE ${index.unique ? \"UNIQUE \" : \"\"}INDEX IF NOT EXISTS ${indexName} ON ${name} (${cols})`,\n );\n }\n }\n\n return statements;\n}\n"]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { toSnakeCase } from './chunk-AEYYTE5I.js';
|
|
2
|
+
|
|
3
|
+
// src/schema/render-mysql.ts
|
|
4
|
+
var quote = (value) => `\`${value.split("`").join("``")}\``;
|
|
5
|
+
function stringType(field, table, spec) {
|
|
6
|
+
const keys = [
|
|
7
|
+
...table.primaryKey ? [table.primaryKey] : [],
|
|
8
|
+
...(table.indexes ?? []).map((index) => index.on)
|
|
9
|
+
].filter((columns) => columns.includes(field));
|
|
10
|
+
if (keys.some((columns) => columns.length > 1)) return "VARCHAR(255)";
|
|
11
|
+
if (keys.length > 0) return "VARCHAR(768)";
|
|
12
|
+
if (spec.default !== void 0) return "VARCHAR(255)";
|
|
13
|
+
return "TEXT";
|
|
14
|
+
}
|
|
15
|
+
function columnType(field, table, spec) {
|
|
16
|
+
switch (spec.type) {
|
|
17
|
+
case "string":
|
|
18
|
+
return stringType(field, table, spec);
|
|
19
|
+
case "number":
|
|
20
|
+
return "INT";
|
|
21
|
+
case "boolean":
|
|
22
|
+
return "TINYINT(1)";
|
|
23
|
+
case "date":
|
|
24
|
+
return "DATETIME(3)";
|
|
25
|
+
case "json":
|
|
26
|
+
return "JSON";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
var defaultClause = (spec) => {
|
|
30
|
+
if (spec.defaultNow) return " DEFAULT CURRENT_TIMESTAMP(3)";
|
|
31
|
+
if (spec.default === void 0) return "";
|
|
32
|
+
if (typeof spec.default === "string") return ` DEFAULT '${spec.default.replace(/'/g, "''")}'`;
|
|
33
|
+
if (typeof spec.default === "boolean") return ` DEFAULT ${spec.default ? 1 : 0}`;
|
|
34
|
+
return ` DEFAULT ${spec.default}`;
|
|
35
|
+
};
|
|
36
|
+
function renderMysqlDdl(schema, prefix = "") {
|
|
37
|
+
const statements = [];
|
|
38
|
+
for (const table of Object.values(schema)) {
|
|
39
|
+
const lines = Object.entries(table.fields).map(([field, spec]) => {
|
|
40
|
+
const nullability = spec.required ? " NOT NULL" : "";
|
|
41
|
+
return ` ${quote(toSnakeCase(field))} ${columnType(field, table, spec)}${nullability}${defaultClause(spec)}`;
|
|
42
|
+
});
|
|
43
|
+
if (table.primaryKey?.length) {
|
|
44
|
+
lines.push(
|
|
45
|
+
` PRIMARY KEY (${table.primaryKey.map((f) => quote(toSnakeCase(f))).join(", ")})`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
for (const index of table.indexes ?? []) {
|
|
49
|
+
const name = quote(prefix + (index.name ?? `${table.tableName}_${index.on.join("_")}_idx`));
|
|
50
|
+
const columns = index.on.map((f) => quote(toSnakeCase(f))).join(", ");
|
|
51
|
+
lines.push(` ${index.unique ? "UNIQUE " : ""}INDEX ${name} (${columns})`);
|
|
52
|
+
}
|
|
53
|
+
statements.push(
|
|
54
|
+
`CREATE TABLE IF NOT EXISTS ${quote(prefix + table.tableName)} (
|
|
55
|
+
${lines.join(",\n")}
|
|
56
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return statements;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { renderMysqlDdl };
|
|
63
|
+
//# sourceMappingURL=chunk-M4R77BJD.js.map
|
|
64
|
+
//# sourceMappingURL=chunk-M4R77BJD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema/render-mysql.ts"],"names":[],"mappings":"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","file":"chunk-M4R77BJD.js","sourcesContent":["import type { FieldDeclaration, SchemaDeclaration, TableDeclaration } from \"../core/plugin\";\nimport { toSnakeCase } from \"./declaration\";\n\nconst quote = (value: string) => `\\`${value.split(\"`\").join(\"``\")}\\``;\n\n/** InnoDB caps a key at 3072 bytes: 768 utf8mb4 chars for one column, 255 each for three. TEXT cannot be a key or carry a default. */\nfunction stringType(field: string, table: TableDeclaration, spec: FieldDeclaration): string {\n const keys = [\n ...(table.primaryKey ? [table.primaryKey] : []),\n ...(table.indexes ?? []).map((index) => index.on),\n ].filter((columns) => columns.includes(field));\n\n if (keys.some((columns) => columns.length > 1)) return \"VARCHAR(255)\";\n if (keys.length > 0) return \"VARCHAR(768)\";\n if (spec.default !== undefined) return \"VARCHAR(255)\";\n return \"TEXT\";\n}\n\nfunction columnType(field: string, table: TableDeclaration, spec: FieldDeclaration): string {\n switch (spec.type) {\n case \"string\":\n return stringType(field, table, spec);\n case \"number\":\n return \"INT\";\n case \"boolean\":\n return \"TINYINT(1)\";\n case \"date\":\n return \"DATETIME(3)\";\n case \"json\":\n return \"JSON\";\n }\n}\n\nconst defaultClause = (spec: FieldDeclaration): string => {\n if (spec.defaultNow) return \" DEFAULT CURRENT_TIMESTAMP(3)\";\n if (spec.default === undefined) return \"\";\n if (typeof spec.default === \"string\") return ` DEFAULT '${spec.default.replace(/'/g, \"''\")}'`;\n if (typeof spec.default === \"boolean\") return ` DEFAULT ${spec.default ? 1 : 0}`;\n return ` DEFAULT ${spec.default}`;\n};\n\n/** MySQL DDL, one CREATE TABLE IF NOT EXISTS per table with indexes inline (no CREATE INDEX IF NOT EXISTS). */\nexport function renderMysqlDdl(schema: SchemaDeclaration, prefix = \"\"): string[] {\n const statements: string[] = [];\n\n for (const table of Object.values(schema)) {\n const lines = Object.entries(table.fields).map(([field, spec]) => {\n const nullability = spec.required ? \" NOT NULL\" : \"\";\n return ` ${quote(toSnakeCase(field))} ${columnType(field, table, spec)}${nullability}${defaultClause(spec)}`;\n });\n\n if (table.primaryKey?.length) {\n lines.push(\n ` PRIMARY KEY (${table.primaryKey.map((f) => quote(toSnakeCase(f))).join(\", \")})`,\n );\n }\n\n for (const index of table.indexes ?? []) {\n const name = quote(prefix + (index.name ?? `${table.tableName}_${index.on.join(\"_\")}_idx`));\n const columns = index.on.map((f) => quote(toSnakeCase(f))).join(\", \");\n lines.push(` ${index.unique ? \"UNIQUE \" : \"\"}INDEX ${name} (${columns})`);\n }\n\n statements.push(\n `CREATE TABLE IF NOT EXISTS ${quote(prefix + table.tableName)} (\\n${lines.join(\",\\n\")}\\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,\n );\n }\n\n return statements;\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { toSnakeCase } from './chunk-
|
|
1
|
+
import { toSnakeCase } from './chunk-AEYYTE5I.js';
|
|
2
2
|
|
|
3
3
|
// src/schema/render-sql.ts
|
|
4
4
|
var SQL_TYPE = {
|
|
@@ -65,6 +65,15 @@ var defaultClause2 = (field) => {
|
|
|
65
65
|
return ` DEFAULT ${field.default}`;
|
|
66
66
|
};
|
|
67
67
|
var indexName = (table, declaration) => declaration.name ?? `${table.tableName}_${declaration.on.join("_")}_idx`;
|
|
68
|
+
function normaliseColumn(row) {
|
|
69
|
+
if ("table" in row) return row;
|
|
70
|
+
return {
|
|
71
|
+
table: String(row.table_name),
|
|
72
|
+
column: String(row.column_name),
|
|
73
|
+
type: String(row.data_type),
|
|
74
|
+
nullable: typeof row.is_nullable === "boolean" ? row.is_nullable : row.is_nullable === "YES"
|
|
75
|
+
};
|
|
76
|
+
}
|
|
68
77
|
var INTROSPECT_SQL = `
|
|
69
78
|
SELECT table_name, column_name, data_type, is_nullable
|
|
70
79
|
FROM information_schema.columns
|
|
@@ -73,7 +82,8 @@ var INTROSPECT_SQL = `
|
|
|
73
82
|
async function planPostgresMigration(introspect, schema, prefix = "") {
|
|
74
83
|
const live = await introspect();
|
|
75
84
|
const byTable = /* @__PURE__ */ new Map();
|
|
76
|
-
for (const
|
|
85
|
+
for (const row of live) {
|
|
86
|
+
const column = normaliseColumn(row);
|
|
77
87
|
const columns = byTable.get(column.table) ?? /* @__PURE__ */ new Map();
|
|
78
88
|
columns.set(column.column, column);
|
|
79
89
|
byTable.set(column.table, columns);
|
|
@@ -195,5 +205,5 @@ ${body}
|
|
|
195
205
|
}
|
|
196
206
|
|
|
197
207
|
export { INTROSPECT_SQL, planPostgresMigration, renderDrizzleSchema, renderPostgresDdl };
|
|
198
|
-
//# sourceMappingURL=chunk-
|
|
199
|
-
//# sourceMappingURL=chunk-
|
|
208
|
+
//# sourceMappingURL=chunk-XIDVLYNQ.js.map
|
|
209
|
+
//# sourceMappingURL=chunk-XIDVLYNQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema/render-sql.ts","../src/schema/migrate-sql.ts","../src/schema/render-drizzle.ts"],"names":["indexName","columns","SQL_TYPE","quote","defaultClause"],"mappings":"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","file":"chunk-XIDVLYNQ.js","sourcesContent":["import type { FieldDeclaration, SchemaDeclaration } from \"../core/plugin\";\nimport { toSnakeCase } from \"./declaration\";\n\nconst SQL_TYPE: Record<FieldDeclaration[\"type\"], string> = {\n string: \"text\",\n number: \"integer\",\n boolean: \"boolean\",\n date: \"timestamptz\",\n json: \"jsonb\",\n};\n\nconst quote = (value: string) => `\"${value.replace(/\"/g, '\"\"')}\"`;\n\nconst defaultClause = (field: FieldDeclaration): string => {\n if (field.defaultNow) return \" DEFAULT now()\";\n if (field.default === undefined) return \"\";\n if (typeof field.default === \"string\") return ` DEFAULT '${field.default.replace(/'/g, \"''\")}'`;\n return ` DEFAULT ${field.default}`;\n};\n\n/** SchemaDeclaration to Postgres DDL, from the same source as the Drizzle renderer. */\nexport function renderPostgresDdl(schema: SchemaDeclaration, prefix = \"\"): string[] {\n const statements: string[] = [];\n\n for (const table of Object.values(schema)) {\n const name = quote(prefix + table.tableName);\n\n const columns = Object.entries(table.fields).map(([field, spec]) => {\n const nullability = spec.required ? \" NOT NULL\" : \"\";\n return ` ${quote(toSnakeCase(field))} ${SQL_TYPE[spec.type]}${nullability}${defaultClause(spec)}`;\n });\n\n if (table.primaryKey?.length) {\n const key = table.primaryKey.map((field) => quote(toSnakeCase(field))).join(\", \");\n columns.push(` PRIMARY KEY (${key})`);\n }\n\n statements.push(`CREATE TABLE IF NOT EXISTS ${name} (\\n${columns.join(\",\\n\")}\\n)`);\n\n for (const declaration of table.indexes ?? []) {\n const indexName = quote(\n prefix + (declaration.name ?? `${table.tableName}_${declaration.on.join(\"_\")}_idx`),\n );\n const columns = declaration.on.map((field) => quote(toSnakeCase(field))).join(\", \");\n const unique = declaration.unique ? \"UNIQUE \" : \"\";\n statements.push(`CREATE ${unique}INDEX IF NOT EXISTS ${indexName} ON ${name} (${columns})`);\n }\n }\n\n return statements;\n}\n","import type { FieldDeclaration, SchemaDeclaration, TableDeclaration } from \"../core/plugin\";\nimport { toSnakeCase } from \"./declaration\";\nimport { renderPostgresDdl } from \"./render-sql\";\n\n/** Introspects a live Postgres schema and emits only what is missing. Additive only: drops and type changes land in `unsupported`. */\n\nconst SQL_TYPE: Record<FieldDeclaration[\"type\"], string> = {\n string: \"text\",\n number: \"integer\",\n boolean: \"boolean\",\n date: \"timestamptz\",\n json: \"jsonb\",\n};\n\n/** What information_schema reports for each of our declared types. */\nconst LIVE_TYPE: Record<FieldDeclaration[\"type\"], readonly string[]> = {\n string: [\"text\", \"character varying\", \"character\"],\n number: [\"integer\", \"bigint\", \"smallint\"],\n boolean: [\"boolean\"],\n date: [\"timestamp with time zone\", \"timestamp without time zone\"],\n json: [\"jsonb\", \"json\"],\n};\n\nconst quote = (value: string) => `\"${value.replace(/\"/g, '\"\"')}\"`;\n\nconst defaultClause = (field: FieldDeclaration): string => {\n if (field.defaultNow) return \" DEFAULT now()\";\n if (field.default === undefined) return \"\";\n if (typeof field.default === \"string\") return ` DEFAULT '${field.default.replace(/'/g, \"''\")}'`;\n return ` DEFAULT ${field.default}`;\n};\n\nconst indexName = (\n table: TableDeclaration,\n declaration: { on: readonly string[]; name?: string },\n) => declaration.name ?? `${table.tableName}_${declaration.on.join(\"_\")}_idx`;\n\nexport type LiveColumn = { table: string; column: string; type: string; nullable: boolean };\n\n/** A row as INTROSPECT_SQL returns it; `LiveColumn` uses the short forms and both are accepted. */\nexport type LiveColumnRow =\n | LiveColumn\n | {\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string | boolean;\n };\n\n/** Accepts either shape, so raw rows and hand-mapped rows both work. */\nfunction normaliseColumn(row: LiveColumnRow): LiveColumn {\n if (\"table\" in row) return row;\n return {\n table: String(row.table_name),\n column: String(row.column_name),\n type: String(row.data_type),\n nullable: typeof row.is_nullable === \"boolean\" ? row.is_nullable : row.is_nullable === \"YES\",\n };\n}\n\nexport type MigrationPlan = {\n /** Run in order. Empty means the live schema already matches. */\n statements: readonly string[];\n /** Differences this cannot express safely. Read them; they need a human. */\n unsupported: readonly string[];\n /** Tables that do not exist yet — these get a full CREATE TABLE. */\n createdTables: readonly string[];\n};\n\n/** Reads the live column list; a function, so this module stays driver-agnostic. */\nexport type Introspector = () => Promise<readonly LiveColumnRow[]>;\n\n/** The query an Introspector should run; raw rows are accepted as they come back. */\nexport const INTROSPECT_SQL = `\n SELECT table_name, column_name, data_type, is_nullable\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n`;\n\nexport async function planPostgresMigration(\n introspect: Introspector,\n schema: SchemaDeclaration,\n prefix = \"\",\n): Promise<MigrationPlan> {\n const live = await introspect();\n\n const byTable = new Map<string, Map<string, LiveColumn>>();\n for (const row of live) {\n const column = normaliseColumn(row);\n const columns = byTable.get(column.table) ?? new Map<string, LiveColumn>();\n columns.set(column.column, column);\n byTable.set(column.table, columns);\n }\n\n const statements: string[] = [];\n const unsupported: string[] = [];\n const createdTables: string[] = [];\n\n for (const table of Object.values(schema)) {\n const qualified = prefix + table.tableName;\n const existing = byTable.get(qualified);\n\n // Absent entirely: the ordinary bootstrap path still applies.\n if (!existing || existing.size === 0) {\n createdTables.push(qualified);\n statements.push(...renderPostgresDdl({ [table.tableName]: table }, prefix));\n continue;\n }\n\n for (const [field, spec] of Object.entries(table.fields)) {\n const column = toSnakeCase(field);\n const found = existing.get(column);\n\n if (!found) {\n // NOT NULL fails on a populated table without a default: added nullable and flagged.\n const canFill = spec.defaultNow || spec.default !== undefined;\n const nullability = spec.required && canFill ? \" NOT NULL\" : \"\";\n\n statements.push(\n `ALTER TABLE ${quote(qualified)} ADD COLUMN IF NOT EXISTS ` +\n `${quote(column)} ${SQL_TYPE[spec.type]}${nullability}${defaultClause(spec)}`,\n );\n\n if (spec.required && !canFill) {\n unsupported.push(\n `${qualified}.${column} is declared required but has no default; ` +\n \"it was added nullable. Backfill it, then ALTER ... SET NOT NULL yourself.\",\n );\n }\n continue;\n }\n\n if (!LIVE_TYPE[spec.type].includes(found.type)) {\n unsupported.push(\n `${qualified}.${column} is ${found.type} but ${spec.type} is declared ` +\n `(${SQL_TYPE[spec.type]}). Changing a column type can lose data — do it yourself.`,\n );\n }\n }\n\n // Indexes are cheap and idempotent, so they are re-issued unconditionally.\n for (const declaration of table.indexes ?? []) {\n const name = quote(prefix + indexName(table, declaration));\n const columns = declaration.on.map((field) => quote(toSnakeCase(field))).join(\", \");\n statements.push(\n `CREATE ${declaration.unique ? \"UNIQUE \" : \"\"}INDEX IF NOT EXISTS ${name} ` +\n `ON ${quote(qualified)} (${columns})`,\n );\n }\n\n for (const column of existing.keys()) {\n const declared = Object.keys(table.fields).some((field) => toSnakeCase(field) === column);\n if (!declared) {\n unsupported.push(\n `${qualified}.${column} exists but is not declared. Left alone — ` +\n \"drop it yourself if it is ours and no longer needed.\",\n );\n }\n }\n }\n\n return { statements, unsupported, createdTables };\n}\n","import type { FieldDeclaration, SchemaDeclaration, TableDeclaration } from \"../core/plugin\";\nimport { toSnakeCase } from \"./declaration\";\n\nconst COLUMN_BUILDER: Record<FieldDeclaration[\"type\"], string> = {\n string: \"text\",\n number: \"integer\",\n boolean: \"boolean\",\n date: \"timestamp\",\n json: \"jsonb\",\n};\n\nconst literal = (value: string | number | boolean) =>\n typeof value === \"string\" ? JSON.stringify(value) : String(value);\n\nfunction renderColumn(name: string, field: FieldDeclaration, isSinglePk: boolean): string {\n const builder = COLUMN_BUILDER[field.type];\n const column = toSnakeCase(name);\n\n let expression =\n field.type === \"date\"\n ? `${builder}(${JSON.stringify(column)}, { withTimezone: true })`\n : `${builder}(${JSON.stringify(column)})`;\n\n if (isSinglePk) return ` ${name}: ${expression}.primaryKey(),`;\n\n if (field.required) expression += \".notNull()\";\n if (field.defaultNow) expression += \".defaultNow()\";\n else if (field.default !== undefined) expression += `.default(${literal(field.default)})`;\n\n return ` ${name}: ${expression},`;\n}\n\nfunction renderTable(key: string, table: TableDeclaration, prefix: string): string {\n const compositePk = (table.primaryKey?.length ?? 0) > 1;\n const singlePk = !compositePk ? table.primaryKey?.[0] : undefined;\n\n const columns = Object.entries(table.fields)\n .map(([name, field]) => renderColumn(name, field, name === singlePk))\n .join(\"\\n\");\n\n const extras: string[] = [];\n\n for (const declaration of table.indexes ?? []) {\n const builder = declaration.unique ? \"uniqueIndex\" : \"index\";\n const name = declaration.name ?? `${table.tableName}_${declaration.on.join(\"_\")}_idx`;\n const columns = declaration.on.map((field) => `table.${field}`).join(\", \");\n extras.push(` ${builder}(${JSON.stringify(prefix + name)}).on(${columns}),`);\n }\n\n if (compositePk) {\n const columns = (table.primaryKey ?? []).map((field) => `table.${field}`).join(\", \");\n extras.push(` primaryKey({ columns: [${columns}] }),`);\n }\n\n const third = extras.length ? `,\\n (table) => [\\n${extras.join(\"\\n\")}\\n ]` : \"\";\n\n return `export const ${key} = pgTable(\\n ${JSON.stringify(prefix + table.tableName)},\\n {\\n${columns}\\n }${third},\\n);`;\n}\n\n/** Renders a SchemaDeclaration to Drizzle pg-core source. */\nexport function renderDrizzleSchema(schema: SchemaDeclaration, prefix = \"\"): string {\n const tables = Object.entries(schema);\n\n const used = new Set<string>([\"pgTable\"]);\n for (const [, table] of tables) {\n for (const field of Object.values(table.fields)) used.add(COLUMN_BUILDER[field.type]);\n for (const declaration of table.indexes ?? []) {\n used.add(declaration.unique ? \"uniqueIndex\" : \"index\");\n }\n if ((table.primaryKey?.length ?? 0) > 1) used.add(\"primaryKey\");\n }\n\n const imports = [...used].sort().join(\", \");\n const body = tables.map(([key, table]) => renderTable(key, table, prefix)).join(\"\\n\\n\");\n\n return `// Generated by easy-ping. Do not edit by hand.\\nimport { ${imports} } from \"drizzle-orm/pg-core\";\\n\\n${body}\\n`;\n}\n"]}
|
package/dist/client.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client/index.ts"],"names":[],"mappings":"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","file":"client.cjs","sourcesContent":["export type NotificationView = {\n id: string;\n userId: string;\n type: string;\n payload: unknown;\n actorId: string | null;\n groupKey: string | null;\n seenAt: string | null;\n readAt: string | null;\n createdAt: string;\n};\n\nexport type NotifyState = {\n notifications: readonly NotificationView[];\n /** Badge count — cleared when the dropdown opens. Server-authoritative. */\n unseenCount: number;\n /** Items still bold. Derived from what is loaded, not a server total. */\n unreadCount: number;\n nextCursor: string | null;\n isLoading: boolean;\n error: Error | null;\n};\n\nexport type NotifyClientOptions = {\n baseUrl?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n limit?: number | undefined;\n pollIntervalMs?: number | undefined;\n maxPollIntervalMs?: number | undefined;\n /** Injectable so the poller can be tested without a DOM. */\n isDocumentHidden?: (() => boolean) | undefined;\n};\n\nconst INITIAL: NotifyState = {\n notifications: [],\n unseenCount: 0,\n unreadCount: 0,\n nextCursor: null,\n isLoading: true,\n error: null,\n};\n\nconst countUnread = (rows: readonly NotificationView[]) =>\n rows.reduce((total, row) => (row.readAt === null ? total + 1 : total), 0);\n\nexport function createNotifyClient(options: NotifyClientOptions = {}) {\n const baseUrl = (options.baseUrl ?? \"/api/notifications\").replace(/\\/$/, \"\");\n const doFetch = options.fetch ?? ((...args) => globalThis.fetch(...args));\n const limit = options.limit ?? 20;\n const pollIntervalMs = options.pollIntervalMs ?? 15_000;\n const maxPollIntervalMs = options.maxPollIntervalMs ?? 5 * 60_000;\n const isHidden =\n options.isDocumentHidden ??\n (() => typeof document !== \"undefined\" && document.visibilityState === \"hidden\");\n\n let state: NotifyState = INITIAL;\n const listeners = new Set<(state: NotifyState) => void>();\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let consecutiveFailures = 0;\n\n // Bumped per mutation: an in-flight poll returns pre-mutation data, which\n // would silently revert the optimistic update.\n let generation = 0;\n\n function set(patch: Partial<NotifyState>) {\n state = { ...state, ...patch };\n state = { ...state, unreadCount: countUnread(state.notifications) };\n for (const listener of listeners) listener(state);\n }\n\n async function request<T>(path: string, init?: RequestInit): Promise<T> {\n const response = await doFetch(`${baseUrl}${path}`, {\n ...init,\n headers: { \"content-type\": \"application/json\", ...init?.headers },\n });\n if (!response.ok)\n throw new Error(`${init?.method ?? \"GET\"} ${path} failed: ${response.status}`);\n return (await response.json()) as T;\n }\n\n async function refresh(): Promise<void> {\n const startedAt = generation;\n\n // One request, not two. The feed carries unseenCount for a first page,\n // which is every poll. A server too old to send it still works: the\n // fallback asks /count the way this always used to.\n const page = await request<{\n notifications: NotificationView[];\n nextCursor: string | null;\n unseenCount?: number;\n }>(`/?limit=${limit}`);\n\n const unseenCount = page.unseenCount ?? (await request<{ unseen: number }>(\"/count\")).unseen;\n\n // A mutation landed while this was in flight, so the response is already\n // stale. Dropping it is correct: the mutation's own optimistic state is\n // newer, and the next poll will reconcile.\n if (generation !== startedAt) return;\n\n set({\n notifications: page.notifications,\n nextCursor: page.nextCursor,\n unseenCount,\n isLoading: false,\n error: null,\n });\n }\n\n let loadingMore: Promise<void> | undefined;\n\n async function loadMore(): Promise<void> {\n if (!state.nextCursor) return;\n\n // Two clicks before the first response lands would both read the same\n // cursor and append the same page twice. Share the in-flight request.\n if (loadingMore) return loadingMore;\n\n const cursor = state.nextCursor;\n\n loadingMore = (async () => {\n try {\n const page = await request<{\n notifications: NotificationView[];\n nextCursor: string | null;\n }>(`/?limit=${limit}&cursor=${encodeURIComponent(cursor)}`);\n\n // Guard against a duplicate arriving anyway — a concurrent refresh may\n // already have pulled some of these into the list.\n const known = new Set(state.notifications.map((row) => row.id));\n\n set({\n notifications: [\n ...state.notifications,\n ...page.notifications.filter((row) => !known.has(row.id)),\n ],\n nextCursor: page.nextCursor,\n });\n } finally {\n loadingMore = undefined;\n }\n })();\n\n return loadingMore;\n }\n\n /** Applies a patch immediately and reverts it if the request fails. */\n async function optimistic(\n apply: (rows: readonly NotificationView[]) => readonly NotificationView[],\n patch: Partial<NotifyState>,\n send: () => Promise<unknown>,\n ): Promise<void> {\n const previous = state;\n generation += 1;\n set({ notifications: apply(state.notifications), ...patch });\n\n try {\n await send();\n } catch (error) {\n generation += 1;\n state = previous;\n for (const listener of listeners) listener(state);\n throw error;\n }\n }\n\n const stamp = () => new Date().toISOString();\n\n async function markAsRead(id: string): Promise<void> {\n await optimistic(\n (rows) =>\n rows.map((row) => (row.id === id ? { ...row, readAt: row.readAt ?? stamp() } : row)),\n {},\n () => request(\"/read\", { method: \"POST\", body: JSON.stringify({ ids: [id] }) }),\n );\n }\n\n async function markAllRead(): Promise<void> {\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, readAt: row.readAt ?? stamp() })),\n {},\n () => request(\"/read-all\", { method: \"POST\" }),\n );\n }\n\n async function markSeen(): Promise<void> {\n // Only what the user could actually have seen. Sending no cutoff would\n // mark notifications that landed after the last poll as seen too.\n const newest = state.notifications[0]?.createdAt;\n\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, seenAt: row.seenAt ?? stamp() })),\n { unseenCount: 0 },\n () =>\n request(\"/seen\", {\n method: \"POST\",\n body: JSON.stringify(newest ? { before: newest } : {}),\n }),\n );\n }\n\n function schedule(delay: number) {\n clearTimeout(timer);\n timer = setTimeout(tick, delay);\n }\n\n async function tick() {\n // A hidden tab still holds an open bell. Polling it burns requests for a\n // UI nobody is looking at.\n if (isHidden()) {\n schedule(pollIntervalMs);\n return;\n }\n\n try {\n await refresh();\n consecutiveFailures = 0;\n } catch (error) {\n consecutiveFailures += 1;\n set({ isLoading: false, error: error instanceof Error ? error : new Error(String(error)) });\n }\n\n // Back off on failure so an outage does not turn every open tab into a\n // retry loop against a struggling server.\n schedule(\n consecutiveFailures === 0\n ? pollIntervalMs\n : Math.min(pollIntervalMs * 2 ** consecutiveFailures, maxPollIntervalMs),\n );\n }\n\n function subscribe(listener: (state: NotifyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n\n // One poller regardless of how many components subscribe — otherwise two\n // bells, or a StrictMode double-mount, double the request rate.\n if (listeners.size === 1) void tick();\n\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n }\n\n return {\n subscribe,\n refresh,\n loadMore,\n markAsRead,\n markAllRead,\n markSeen,\n getState: () => state,\n };\n}\n\nexport type NotifyClient = ReturnType<typeof createNotifyClient>;\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/client/index.ts"],"names":[],"mappings":"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","file":"client.cjs","sourcesContent":["export type NotificationView = {\n id: string;\n userId: string;\n type: string;\n payload: unknown;\n actorId: string | null;\n groupKey: string | null;\n seenAt: string | null;\n readAt: string | null;\n createdAt: string;\n};\n\nexport type NotifyState = {\n notifications: readonly NotificationView[];\n /** Badge count — cleared when the dropdown opens. Server-authoritative. */\n unseenCount: number;\n /** Items still bold. Derived from what is loaded, not a server total. */\n unreadCount: number;\n nextCursor: string | null;\n isLoading: boolean;\n error: Error | null;\n};\n\nexport type NotifyClientOptions = {\n baseUrl?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n limit?: number | undefined;\n pollIntervalMs?: number | undefined;\n maxPollIntervalMs?: number | undefined;\n /** Injectable so the poller can be tested without a DOM. */\n isDocumentHidden?: (() => boolean) | undefined;\n};\n\nconst INITIAL: NotifyState = {\n notifications: [],\n unseenCount: 0,\n unreadCount: 0,\n nextCursor: null,\n isLoading: true,\n error: null,\n};\n\nconst countUnread = (rows: readonly NotificationView[]) =>\n rows.reduce((total, row) => (row.readAt === null ? total + 1 : total), 0);\n\nexport function createNotifyClient(options: NotifyClientOptions = {}) {\n const baseUrl = (options.baseUrl ?? \"/api/notifications\").replace(/\\/$/, \"\");\n const doFetch = options.fetch ?? ((...args) => globalThis.fetch(...args));\n const limit = options.limit ?? 20;\n const pollIntervalMs = options.pollIntervalMs ?? 15_000;\n const maxPollIntervalMs = options.maxPollIntervalMs ?? 5 * 60_000;\n const isHidden =\n options.isDocumentHidden ??\n (() => typeof document !== \"undefined\" && document.visibilityState === \"hidden\");\n\n let state: NotifyState = INITIAL;\n const listeners = new Set<(state: NotifyState) => void>();\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let consecutiveFailures = 0;\n\n // Bumped per mutation so an in-flight poll cannot revert an optimistic update.\n let generation = 0;\n\n function set(patch: Partial<NotifyState>) {\n state = { ...state, ...patch };\n state = { ...state, unreadCount: countUnread(state.notifications) };\n for (const listener of listeners) listener(state);\n }\n\n async function request<T>(path: string, init?: RequestInit): Promise<T> {\n const response = await doFetch(`${baseUrl}${path}`, {\n ...init,\n headers: { \"content-type\": \"application/json\", ...init?.headers },\n });\n if (!response.ok)\n throw new Error(`${init?.method ?? \"GET\"} ${path} failed: ${response.status}`);\n return (await response.json()) as T;\n }\n\n async function refresh(): Promise<void> {\n const startedAt = generation;\n\n // One request per poll; /count is only the fallback for a server too old to send unseenCount.\n const page = await request<{\n notifications: NotificationView[];\n nextCursor: string | null;\n unseenCount?: number;\n }>(`/?limit=${limit}`);\n\n const unseenCount = page.unseenCount ?? (await request<{ unseen: number }>(\"/count\")).unseen;\n\n // A mutation landed while this was in flight; the response is stale.\n if (generation !== startedAt) return;\n\n set({\n notifications: page.notifications,\n nextCursor: page.nextCursor,\n unseenCount,\n isLoading: false,\n error: null,\n });\n }\n\n let loadingMore: Promise<void> | undefined;\n\n async function loadMore(): Promise<void> {\n if (!state.nextCursor) return;\n\n // Share the in-flight request so two clicks cannot append the same page twice.\n if (loadingMore) return loadingMore;\n\n const cursor = state.nextCursor;\n\n loadingMore = (async () => {\n try {\n const page = await request<{\n notifications: NotificationView[];\n nextCursor: string | null;\n }>(`/?limit=${limit}&cursor=${encodeURIComponent(cursor)}`);\n\n const known = new Set(state.notifications.map((row) => row.id));\n\n set({\n notifications: [\n ...state.notifications,\n ...page.notifications.filter((row) => !known.has(row.id)),\n ],\n nextCursor: page.nextCursor,\n });\n } finally {\n loadingMore = undefined;\n }\n })();\n\n return loadingMore;\n }\n\n /** Applies a patch immediately and reverts it if the request fails. */\n async function optimistic(\n apply: (rows: readonly NotificationView[]) => readonly NotificationView[],\n patch: Partial<NotifyState>,\n send: () => Promise<unknown>,\n ): Promise<void> {\n const previous = state;\n generation += 1;\n set({ notifications: apply(state.notifications), ...patch });\n\n try {\n await send();\n } catch (error) {\n generation += 1;\n state = previous;\n for (const listener of listeners) listener(state);\n throw error;\n }\n }\n\n const stamp = () => new Date().toISOString();\n\n async function markAsRead(id: string): Promise<void> {\n await optimistic(\n (rows) =>\n rows.map((row) => (row.id === id ? { ...row, readAt: row.readAt ?? stamp() } : row)),\n {},\n () => request(\"/read\", { method: \"POST\", body: JSON.stringify({ ids: [id] }) }),\n );\n }\n\n async function markAllRead(): Promise<void> {\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, readAt: row.readAt ?? stamp() })),\n {},\n () => request(\"/read-all\", { method: \"POST\" }),\n );\n }\n\n async function markSeen(): Promise<void> {\n // Only what the user could have seen: no cutoff would mark newer arrivals seen too.\n const newest = state.notifications[0]?.createdAt;\n\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, seenAt: row.seenAt ?? stamp() })),\n { unseenCount: 0 },\n () =>\n request(\"/seen\", {\n method: \"POST\",\n body: JSON.stringify(newest ? { before: newest } : {}),\n }),\n );\n }\n\n function schedule(delay: number) {\n clearTimeout(timer);\n timer = setTimeout(tick, delay);\n }\n\n async function tick() {\n // A hidden tab is not looking at the bell.\n if (isHidden()) {\n schedule(pollIntervalMs);\n return;\n }\n\n try {\n await refresh();\n consecutiveFailures = 0;\n } catch (error) {\n consecutiveFailures += 1;\n set({ isLoading: false, error: error instanceof Error ? error : new Error(String(error)) });\n }\n\n // Back off on failure so an outage does not become a retry storm.\n schedule(\n consecutiveFailures === 0\n ? pollIntervalMs\n : Math.min(pollIntervalMs * 2 ** consecutiveFailures, maxPollIntervalMs),\n );\n }\n\n function subscribe(listener: (state: NotifyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n\n // One poller regardless of how many components subscribe.\n if (listeners.size === 1) void tick();\n\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n }\n\n return {\n subscribe,\n refresh,\n loadMore,\n markAsRead,\n markAllRead,\n markSeen,\n getState: () => state,\n };\n}\n\nexport type NotifyClient = ReturnType<typeof createNotifyClient>;\n"]}
|