easy-ping 0.6.1 → 0.8.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 +24 -4
- package/dist/{adapter-D3lTy-4E.d.cts → adapter-CV5Yt2c6.d.cts} +1 -1
- package/dist/{adapter-D3lTy-4E.d.ts → adapter-CV5Yt2c6.d.ts} +1 -1
- package/dist/adapters/drizzle.d.cts +1 -1
- package/dist/adapters/drizzle.d.ts +1 -1
- package/dist/adapters/mongodb.d.cts +2 -2
- package/dist/adapters/mongodb.d.ts +2 -2
- package/dist/adapters/mysql.d.cts +2 -2
- package/dist/adapters/mysql.d.ts +2 -2
- package/dist/adapters/postgres.d.cts +2 -2
- package/dist/adapters/postgres.d.ts +2 -2
- package/dist/adapters/sqlite.d.cts +2 -2
- package/dist/adapters/sqlite.d.ts +2 -2
- package/dist/chunk-R3BE67L7.js +13 -0
- package/dist/chunk-R3BE67L7.js.map +1 -0
- package/dist/chunk-SSHVZHSJ.js +100 -0
- package/dist/chunk-SSHVZHSJ.js.map +1 -0
- package/dist/client.cjs +7 -2
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +7 -2
- package/dist/client.js.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -11
- package/dist/index.js.map +1 -1
- package/dist/{plugin-CHd2ZiGC.d.cts → plugin-6S8tpH0n.d.cts} +1 -1
- package/dist/{plugin-Cg9usuUi.d.ts → plugin-C4fikLl7.d.ts} +1 -1
- package/dist/plugins/digests.d.cts +2 -2
- package/dist/plugins/digests.d.ts +2 -2
- package/dist/plugins/mobile-push.cjs +307 -0
- package/dist/plugins/mobile-push.cjs.map +1 -0
- package/dist/plugins/mobile-push.d.cts +117 -0
- package/dist/plugins/mobile-push.d.ts +117 -0
- package/dist/plugins/mobile-push.js +261 -0
- package/dist/plugins/mobile-push.js.map +1 -0
- package/dist/plugins/preferences-client.d.cts +2 -2
- package/dist/plugins/preferences-client.d.ts +2 -2
- package/dist/plugins/preferences-react.d.cts +1 -1
- package/dist/plugins/preferences-react.d.ts +1 -1
- package/dist/plugins/preferences.cjs +9 -1
- package/dist/plugins/preferences.cjs.map +1 -1
- package/dist/plugins/preferences.d.cts +2 -2
- package/dist/plugins/preferences.d.ts +2 -2
- package/dist/plugins/preferences.js +9 -1
- package/dist/plugins/preferences.js.map +1 -1
- package/dist/plugins/push.d.cts +2 -2
- package/dist/plugins/push.d.ts +2 -2
- package/dist/plugins/telegram.cjs +379 -0
- package/dist/plugins/telegram.cjs.map +1 -0
- package/dist/plugins/telegram.d.cts +153 -0
- package/dist/plugins/telegram.d.ts +153 -0
- package/dist/plugins/telegram.js +313 -0
- package/dist/plugins/telegram.js.map +1 -0
- package/dist/providers/expo-push.cjs +114 -0
- package/dist/providers/expo-push.cjs.map +1 -0
- package/dist/providers/expo-push.d.cts +61 -0
- package/dist/providers/expo-push.d.ts +61 -0
- package/dist/providers/expo-push.js +109 -0
- package/dist/providers/expo-push.js.map +1 -0
- package/dist/providers/telegram.cjs +103 -0
- package/dist/providers/telegram.cjs.map +1 -0
- package/dist/providers/telegram.d.cts +83 -0
- package/dist/providers/telegram.d.ts +83 -0
- package/dist/providers/telegram.js +3 -0
- package/dist/providers/telegram.js.map +1 -0
- package/dist/providers/web-push.d.cts +2 -2
- package/dist/providers/web-push.d.ts +2 -2
- package/dist/react-native.cjs +637 -0
- package/dist/react-native.cjs.map +1 -0
- package/dist/react-native.d.cts +145 -0
- package/dist/react-native.d.ts +145 -0
- package/dist/react-native.js +609 -0
- package/dist/react-native.js.map +1 -0
- package/dist/react.cjs +7 -2
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +7 -2
- package/dist/react.js.map +1 -1
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/package.json +38 -4
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# easy-ping
|
|
4
4
|
|
|
5
5
|
**Own your notifications.**
|
|
6
|
-
In-app inbox, transactional email
|
|
6
|
+
In-app inbox, transactional email, web push and Telegram — running inside your app, stored in your
|
|
7
7
|
database, with no per-notification bill.
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/easy-ping)
|
|
@@ -47,7 +47,7 @@ hand-rolling it badly.
|
|
|
47
47
|
| **Cost per notification** | none | none, you pay for servers | metered per send |
|
|
48
48
|
| **License** | MIT, all of it | MIT core, commercial modules | proprietary |
|
|
49
49
|
| **Visual workflow editor** | **no**, a notification is code | yes | yes |
|
|
50
|
-
| **Channels out of the box** | **in-app, email, web push** | dozens | dozens |
|
|
50
|
+
| **Channels out of the box** | **in-app, email, web push, Telegram** | dozens | dozens |
|
|
51
51
|
|
|
52
52
|
The last two rows are the trade. If you need a workflow editor a non-developer can edit, use one
|
|
53
53
|
of the others — they are good tools solving a bigger problem.
|
|
@@ -56,7 +56,7 @@ of the others — they are good tools solving a bigger problem.
|
|
|
56
56
|
Every "no queue required" decision below follows from that.
|
|
57
57
|
|
|
58
58
|
> **Status: published, pre-1.0.** The core pipeline, all three adapters, the React client and the
|
|
59
|
-
> preferences, digests and
|
|
59
|
+
> preferences, digests, push and telegram plugins are covered by tests against real databases. Web push is
|
|
60
60
|
> verified end to end against Mozilla's production push service and cross-checked against
|
|
61
61
|
> `http_ece`. The bell updates over a server-sent event stream with polling as the fallback;
|
|
62
62
|
> batching is not built, and the Resend provider has only been exercised against a stub. Minor
|
|
@@ -315,6 +315,8 @@ import { handlePush } from "easy-ping/sw";
|
|
|
315
315
|
self.addEventListener("push", (event) => handlePush(event));
|
|
316
316
|
```
|
|
317
317
|
|
|
318
|
+
**React Native.** The same client ships as `easy-ping/react-native`: `useNotifications` wired to `AppState`, a live stream when you pass `fetch` from `expo/fetch`, polling otherwise, and `registerMobilePushDevice` for native push. `examples/expo` is a complete screen.
|
|
319
|
+
|
|
318
320
|
### 7. Wire the cron
|
|
319
321
|
|
|
320
322
|
```
|
|
@@ -379,10 +381,26 @@ single replica.
|
|
|
379
381
|
| `inApp` | ✅ built in, on by default, needs no provider |
|
|
380
382
|
| `email` | ✅ Resend provider; the interface is open for others |
|
|
381
383
|
| `push` | ✅ push plugin + `webPush()` — VAPID and aes128gcm on Web Crypto, so it runs on edge too |
|
|
384
|
+
| `telegram` | ✅ telegram plugin + `telegramBot()` — one-tap linking through the bot, webhook or long-poll |
|
|
385
|
+
| `mobilePush` | ✅ mobile-push plugin + `expoPush()` — iOS and Android through Expo's push service, receipts prune uninstalls |
|
|
382
386
|
| `sms` | ⬜ not implemented |
|
|
383
387
|
| `slack` | ⬜ not implemented |
|
|
384
388
|
|
|
385
|
-
A channel is usable when core carries it (`inApp`, `email`) or a plugin declares it and can `deliver` it. That is how push
|
|
389
|
+
A channel is usable when core carries it (`inApp`, `email`) or a plugin declares it and can `deliver` it. That is how push and telegram work, and how sms and slack will.
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
import { telegram } from "easy-ping/plugins/telegram";
|
|
393
|
+
import { telegramBot } from "easy-ping/providers/telegram";
|
|
394
|
+
|
|
395
|
+
telegram({
|
|
396
|
+
provider: telegramBot({ token: process.env.TELEGRAM_BOT_TOKEN! }),
|
|
397
|
+
botUsername: "your_bot",
|
|
398
|
+
webhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET, // or plugin.poll() in development
|
|
399
|
+
render: ({ payload }) => ({ text: `<b>${escapeHtml(payload.authorName)}</b> replied` }),
|
|
400
|
+
});
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
`POST /telegram/link` returns a `t.me/<bot>?start=<code>` link; the user taps it, the bot stores the chat, and every `send()` with `telegram` in its channels reaches it. Blocked chats prune themselves.
|
|
386
404
|
|
|
387
405
|
Declaring a channel nothing can carry **warns at startup** and reports `skipped: "channel-unavailable"` — deliberately distinct from `"no-channels"`, so a missing provider never looks like a user opt-out.
|
|
388
406
|
|
|
@@ -475,6 +493,8 @@ Defaults to the last 24 hours, capped at 1000 rows. Wire it to an admin page or
|
|
|
475
493
|
| ✅ digests plugin, timezone-aware | |
|
|
476
494
|
| ✅ push plugin + web-push provider | VAPID + RFC 8291, no node:crypto |
|
|
477
495
|
| ✅ push verified against a live push service | Mozilla autopush, plus a cross-check against `http_ece` |
|
|
496
|
+
| ✅ telegram plugin + bot provider | one-tap linking, webhook or long-poll, blocked chats pruned |
|
|
497
|
+
| ✅ mobile push plugin + Expo provider, React Native client entry | token registry, batched sends, receipts prune uninstalls; `AppState`-aware hook |
|
|
478
498
|
| ✅ scoped plugin storage, so plugins own their tables | |
|
|
479
499
|
| ✅ additive schema migrations for the raw-SQL path | `planPostgresMigration()`; MySQL and SQLite are bootstrap-only for now |
|
|
480
500
|
| ✅ failed deliveries reachable from the instance | `notify.getFailedDeliveries()` |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type Channel = "inApp" | "email" | "push" | "sms" | "slack";
|
|
1
|
+
type Channel = "inApp" | "email" | "push" | "mobilePush" | "telegram" | "sms" | "slack";
|
|
2
2
|
type DeliveryStatus = "pending" | "claimed" | "sent" | "failed" | "skipped";
|
|
3
3
|
type DeliveryMode = "inline" | "deferred" | "worker" | "cron";
|
|
4
4
|
type Frequency = "instant" | "daily" | "weekly" | "off";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type Channel = "inApp" | "email" | "push" | "sms" | "slack";
|
|
1
|
+
type Channel = "inApp" | "email" | "push" | "mobilePush" | "telegram" | "sms" | "slack";
|
|
2
2
|
type DeliveryStatus = "pending" | "claimed" | "sent" | "failed" | "skipped";
|
|
3
3
|
type DeliveryMode = "inline" | "deferred" | "worker" | "cron";
|
|
4
4
|
type Frequency = "instant" | "daily" | "weekly" | "off";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as DatabaseAdapter, C as Channel, a as DeliveryStatus, F as Frequency } from '../adapter-
|
|
1
|
+
import { D as DatabaseAdapter, C as Channel, a as DeliveryStatus, F as Frequency } from '../adapter-CV5Yt2c6.cjs';
|
|
2
2
|
import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
|
|
3
3
|
|
|
4
4
|
type DrizzleAdapterOptions = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as DatabaseAdapter, C as Channel, a as DeliveryStatus, F as Frequency } from '../adapter-
|
|
1
|
+
import { D as DatabaseAdapter, C as Channel, a as DeliveryStatus, F as Frequency } from '../adapter-CV5Yt2c6.js';
|
|
2
2
|
import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
|
|
3
3
|
|
|
4
4
|
type DrizzleAdapterOptions = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as DatabaseAdapter } from '../adapter-
|
|
2
|
-
import { S as SchemaDeclaration } from '../plugin-
|
|
1
|
+
import { D as DatabaseAdapter } from '../adapter-CV5Yt2c6.cjs';
|
|
2
|
+
import { S as SchemaDeclaration } from '../plugin-6S8tpH0n.cjs';
|
|
3
3
|
import { S as Signals } from '../signals-kPmOlKDX.cjs';
|
|
4
4
|
|
|
5
5
|
type MongoAdapterOptions = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as DatabaseAdapter } from '../adapter-
|
|
2
|
-
import { S as SchemaDeclaration } from '../plugin-
|
|
1
|
+
import { D as DatabaseAdapter } from '../adapter-CV5Yt2c6.js';
|
|
2
|
+
import { S as SchemaDeclaration } from '../plugin-C4fikLl7.js';
|
|
3
3
|
import { S as Signals } from '../signals-kPmOlKDX.js';
|
|
4
4
|
|
|
5
5
|
type MongoAdapterOptions = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { S as SqlAdapterOptions, a as SqlExec } from '../adapter-B7R4rXtJ.cjs';
|
|
2
2
|
export { b as MysqlResult } from '../adapter-B7R4rXtJ.cjs';
|
|
3
|
-
import { D as DatabaseAdapter } from '../adapter-
|
|
4
|
-
import { S as SchemaDeclaration } from '../plugin-
|
|
3
|
+
import { D as DatabaseAdapter } from '../adapter-CV5Yt2c6.cjs';
|
|
4
|
+
import { S as SchemaDeclaration } from '../plugin-6S8tpH0n.cjs';
|
|
5
5
|
|
|
6
6
|
type MysqlAdapterOptions = SqlAdapterOptions;
|
|
7
7
|
/**
|
package/dist/adapters/mysql.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { S as SqlAdapterOptions, a as SqlExec } from '../adapter-B7R4rXtJ.js';
|
|
2
2
|
export { b as MysqlResult } from '../adapter-B7R4rXtJ.js';
|
|
3
|
-
import { D as DatabaseAdapter } from '../adapter-
|
|
4
|
-
import { S as SchemaDeclaration } from '../plugin-
|
|
3
|
+
import { D as DatabaseAdapter } from '../adapter-CV5Yt2c6.js';
|
|
4
|
+
import { S as SchemaDeclaration } from '../plugin-C4fikLl7.js';
|
|
5
5
|
|
|
6
6
|
type MysqlAdapterOptions = SqlAdapterOptions;
|
|
7
7
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as DatabaseAdapter } from '../adapter-
|
|
2
|
-
import { S as SchemaDeclaration } from '../plugin-
|
|
1
|
+
import { D as DatabaseAdapter } from '../adapter-CV5Yt2c6.cjs';
|
|
2
|
+
import { S as SchemaDeclaration } from '../plugin-6S8tpH0n.cjs';
|
|
3
3
|
import { S as Signals } from '../signals-kPmOlKDX.cjs';
|
|
4
4
|
|
|
5
5
|
type Row = Record<string, unknown>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as DatabaseAdapter } from '../adapter-
|
|
2
|
-
import { S as SchemaDeclaration } from '../plugin-
|
|
1
|
+
import { D as DatabaseAdapter } from '../adapter-CV5Yt2c6.js';
|
|
2
|
+
import { S as SchemaDeclaration } from '../plugin-C4fikLl7.js';
|
|
3
3
|
import { S as Signals } from '../signals-kPmOlKDX.js';
|
|
4
4
|
|
|
5
5
|
type Row = Record<string, unknown>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { S as SqlAdapterOptions, a as SqlExec } from '../adapter-B7R4rXtJ.cjs';
|
|
2
2
|
export { b as SqliteResult } from '../adapter-B7R4rXtJ.cjs';
|
|
3
|
-
import { D as DatabaseAdapter } from '../adapter-
|
|
4
|
-
import { S as SchemaDeclaration } from '../plugin-
|
|
3
|
+
import { D as DatabaseAdapter } from '../adapter-CV5Yt2c6.cjs';
|
|
4
|
+
import { S as SchemaDeclaration } from '../plugin-6S8tpH0n.cjs';
|
|
5
5
|
|
|
6
6
|
type SqliteAdapterOptions = SqlAdapterOptions;
|
|
7
7
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { S as SqlAdapterOptions, a as SqlExec } from '../adapter-B7R4rXtJ.js';
|
|
2
2
|
export { b as SqliteResult } from '../adapter-B7R4rXtJ.js';
|
|
3
|
-
import { D as DatabaseAdapter } from '../adapter-
|
|
4
|
-
import { S as SchemaDeclaration } from '../plugin-
|
|
3
|
+
import { D as DatabaseAdapter } from '../adapter-CV5Yt2c6.js';
|
|
4
|
+
import { S as SchemaDeclaration } from '../plugin-C4fikLl7.js';
|
|
5
5
|
|
|
6
6
|
type SqliteAdapterOptions = SqlAdapterOptions;
|
|
7
7
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/core/html.ts
|
|
2
|
+
var ESCAPES = {
|
|
3
|
+
"&": "&",
|
|
4
|
+
"<": "<",
|
|
5
|
+
">": ">",
|
|
6
|
+
'"': """,
|
|
7
|
+
"'": "'"
|
|
8
|
+
};
|
|
9
|
+
var escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char);
|
|
10
|
+
|
|
11
|
+
export { escapeHtml };
|
|
12
|
+
//# sourceMappingURL=chunk-R3BE67L7.js.map
|
|
13
|
+
//# sourceMappingURL=chunk-R3BE67L7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/html.ts"],"names":[],"mappings":";AAAA,IAAM,OAAA,GAAkC;AAAA,EACtC,GAAA,EAAK,OAAA;AAAA,EACL,GAAA,EAAK,MAAA;AAAA,EACL,GAAA,EAAK,MAAA;AAAA,EACL,GAAA,EAAK,QAAA;AAAA,EACL,GAAA,EAAK;AACP,CAAA;AAGO,IAAM,UAAA,GAAa,CAAC,KAAA,KACzB,MAAA,CAAO,SAAS,EAAE,CAAA,CAAE,OAAA,CAAQ,UAAA,EAAY,CAAC,IAAA,KAAS,OAAA,CAAQ,IAAI,KAAK,IAAI","file":"chunk-R3BE67L7.js","sourcesContent":["const ESCAPES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\n/** For template literals: anything a user typed goes through this, or their HTML ships from your domain. */\nexport const escapeHtml = (value: unknown): string =>\n String(value ?? \"\").replace(/[&<>\"']/g, (char) => ESCAPES[char] ?? char);\n"]}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// src/providers/telegram/index.ts
|
|
2
|
+
var telegramStartLink = (botUsername, code) => `https://t.me/${botUsername.replace(/^@/, "")}?start=${encodeURIComponent(code)}`;
|
|
3
|
+
function telegramBot(options) {
|
|
4
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
5
|
+
const base = `${(options.apiBase ?? "https://api.telegram.org").replace(/\/$/, "")}/bot${options.token}`;
|
|
6
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
7
|
+
const redact = (text) => text.split(options.token).join("<token>");
|
|
8
|
+
async function call(method, body, signal, budgetMs = timeoutMs) {
|
|
9
|
+
const timeout = AbortSignal.timeout(budgetMs);
|
|
10
|
+
const merged = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
11
|
+
const response = await doFetch(`${base}/${method}`, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: { "content-type": "application/json" },
|
|
14
|
+
body: JSON.stringify(body),
|
|
15
|
+
signal: merged
|
|
16
|
+
});
|
|
17
|
+
const data = await response.json().catch(() => ({ ok: false }));
|
|
18
|
+
return { status: response.status, data };
|
|
19
|
+
}
|
|
20
|
+
async function must(method, body) {
|
|
21
|
+
const { data } = await call(method, body);
|
|
22
|
+
if (!data.ok)
|
|
23
|
+
throw new Error(redact(`telegram ${method} failed: ${data.description ?? "unknown"}`));
|
|
24
|
+
return data.result;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
name: "telegram",
|
|
28
|
+
async send(message) {
|
|
29
|
+
const body = {
|
|
30
|
+
chat_id: message.chatId,
|
|
31
|
+
text: message.text,
|
|
32
|
+
parse_mode: "HTML",
|
|
33
|
+
link_preview_options: { is_disabled: true }
|
|
34
|
+
};
|
|
35
|
+
if (message.buttons?.length) {
|
|
36
|
+
body.reply_markup = {
|
|
37
|
+
inline_keyboard: [
|
|
38
|
+
message.buttons.map((button) => ({ text: button.text, url: button.url }))
|
|
39
|
+
]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
let status;
|
|
43
|
+
let data;
|
|
44
|
+
try {
|
|
45
|
+
({ status, data } = await call(
|
|
46
|
+
"sendMessage",
|
|
47
|
+
body,
|
|
48
|
+
message.signal
|
|
49
|
+
));
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
52
|
+
return { ok: false, error: redact(text), retryable: true, gone: false };
|
|
53
|
+
}
|
|
54
|
+
if (data.ok) return { ok: true, messageId: data.result.message_id };
|
|
55
|
+
const description = redact(data.description ?? `HTTP ${status}`);
|
|
56
|
+
const code = data.error_code ?? status;
|
|
57
|
+
if (code === 429) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: description,
|
|
61
|
+
retryable: true,
|
|
62
|
+
gone: false,
|
|
63
|
+
retryAfterSeconds: data.parameters?.retry_after
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const gone = code === 403 || code === 400 && /chat not found|user is deactivated|bot was kicked/i.test(description);
|
|
67
|
+
return { ok: false, error: description, retryable: code >= 500, gone };
|
|
68
|
+
},
|
|
69
|
+
async getUpdates({ offset, timeoutSeconds = 25, signal }) {
|
|
70
|
+
const { data } = await call(
|
|
71
|
+
"getUpdates",
|
|
72
|
+
{
|
|
73
|
+
...offset === void 0 ? {} : { offset },
|
|
74
|
+
timeout: timeoutSeconds,
|
|
75
|
+
allowed_updates: ["message", "my_chat_member"]
|
|
76
|
+
},
|
|
77
|
+
signal,
|
|
78
|
+
(timeoutSeconds + 10) * 1e3
|
|
79
|
+
);
|
|
80
|
+
if (!data.ok)
|
|
81
|
+
throw new Error(redact(`telegram getUpdates failed: ${data.description ?? "unknown"}`));
|
|
82
|
+
return data.result;
|
|
83
|
+
},
|
|
84
|
+
setWebhook: async (url, secretToken) => {
|
|
85
|
+
await must("setWebhook", {
|
|
86
|
+
url,
|
|
87
|
+
secret_token: secretToken,
|
|
88
|
+
allowed_updates: ["message", "my_chat_member"]
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
deleteWebhook: async () => {
|
|
92
|
+
await must("deleteWebhook", {});
|
|
93
|
+
},
|
|
94
|
+
getMe: () => must("getMe", {})
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { telegramBot, telegramStartLink };
|
|
99
|
+
//# sourceMappingURL=chunk-SSHVZHSJ.js.map
|
|
100
|
+
//# sourceMappingURL=chunk-SSHVZHSJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/providers/telegram/index.ts"],"names":[],"mappings":"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","file":"chunk-SSHVZHSJ.js","sourcesContent":["export type TelegramButton = { text: string; url: string };\n\nexport type TelegramMessage = {\n chatId: string;\n /** Telegram HTML: <b>, <i>, <a href>, <code>, <pre>. Escape user text with escapeHtml(). */\n text: string;\n buttons?: readonly TelegramButton[] | undefined;\n signal?: AbortSignal | undefined;\n};\n\nexport type TelegramSendResult =\n | { ok: true; messageId: number }\n | {\n ok: false;\n error: string;\n /** Worth another attempt: rate limit, 5xx, network. */\n retryable: boolean;\n /** The chat can never receive again (user blocked the bot, chat deleted). Prune it. */\n gone: boolean;\n retryAfterSeconds?: number | undefined;\n };\n\n/** The slice of a Bot API update the plugin reads. Everything else passes through untouched. */\nexport type TelegramUpdate = {\n update_id: number;\n message?: {\n text?: string;\n chat: { id: number | string; type: string; username?: string; title?: string };\n from?: { id: number; username?: string; first_name?: string };\n };\n my_chat_member?: {\n chat: { id: number | string; type: string; username?: string; title?: string };\n new_chat_member: { status: string };\n };\n};\n\nexport type TelegramProvider = {\n name: string;\n send(message: TelegramMessage): Promise<TelegramSendResult>;\n /** Long-polls for updates. For development or hosts without a public URL. */\n getUpdates(options: {\n offset?: number | undefined;\n timeoutSeconds?: number | undefined;\n signal?: AbortSignal | undefined;\n }): Promise<TelegramUpdate[]>;\n /** Points Telegram at your webhook route. Run once per deployment URL. */\n setWebhook(url: string, secretToken: string): Promise<void>;\n deleteWebhook(): Promise<void>;\n getMe(): Promise<{ id: number; username: string }>;\n};\n\nexport type TelegramBotOptions = {\n /** From @BotFather. Never appears in errors or logs. */\n token: string;\n fetch?: typeof globalThis.fetch | undefined;\n /** Default https://api.telegram.org. A local fake for tests. */\n apiBase?: string | undefined;\n timeoutMs?: number | undefined;\n};\n\ntype ApiResponse<T> =\n | { ok: true; result: T }\n | { ok: false; description?: string; error_code?: number; parameters?: { retry_after?: number } };\n\n/** Deep link that opens the bot with a start parameter. Codes are [A-Za-z0-9_-], at most 64 chars. */\nexport const telegramStartLink = (botUsername: string, code: string) =>\n `https://t.me/${botUsername.replace(/^@/, \"\")}?start=${encodeURIComponent(code)}`;\n\n/** Bot API over fetch, so it runs on edge runtimes. Failures are classified, never thrown, on send. */\nexport function telegramBot(options: TelegramBotOptions): TelegramProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const base = `${(options.apiBase ?? \"https://api.telegram.org\").replace(/\\/$/, \"\")}/bot${options.token}`;\n const timeoutMs = options.timeoutMs ?? 10_000;\n const redact = (text: string) => text.split(options.token).join(\"<token>\");\n\n async function call<T>(\n method: string,\n body: Record<string, unknown>,\n signal?: AbortSignal,\n budgetMs = timeoutMs,\n ): Promise<{ status: number; data: ApiResponse<T> }> {\n const timeout = AbortSignal.timeout(budgetMs);\n const merged = signal ? AbortSignal.any([signal, timeout]) : timeout;\n const response = await doFetch(`${base}/${method}`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: merged,\n });\n const data = (await response.json().catch(() => ({ ok: false }))) as ApiResponse<T>;\n return { status: response.status, data };\n }\n\n async function must<T>(method: string, body: Record<string, unknown>): Promise<T> {\n const { data } = await call<T>(method, body);\n if (!data.ok)\n throw new Error(redact(`telegram ${method} failed: ${data.description ?? \"unknown\"}`));\n return data.result;\n }\n\n return {\n name: \"telegram\",\n\n async send(message) {\n const body: Record<string, unknown> = {\n chat_id: message.chatId,\n text: message.text,\n parse_mode: \"HTML\",\n link_preview_options: { is_disabled: true },\n };\n if (message.buttons?.length) {\n body.reply_markup = {\n inline_keyboard: [\n message.buttons.map((button) => ({ text: button.text, url: button.url })),\n ],\n };\n }\n\n let status: number;\n let data: ApiResponse<{ message_id: number }>;\n try {\n ({ status, data } = await call<{ message_id: number }>(\n \"sendMessage\",\n body,\n message.signal,\n ));\n } catch (error) {\n const text = error instanceof Error ? error.message : String(error);\n return { ok: false, error: redact(text), retryable: true, gone: false };\n }\n\n if (data.ok) return { ok: true, messageId: data.result.message_id };\n\n const description = redact(data.description ?? `HTTP ${status}`);\n const code = data.error_code ?? status;\n if (code === 429) {\n return {\n ok: false,\n error: description,\n retryable: true,\n gone: false,\n retryAfterSeconds: data.parameters?.retry_after,\n };\n }\n // 403: the user blocked the bot or was deactivated. 400 \"chat not found\": deleted or never existed.\n const gone =\n code === 403 ||\n (code === 400 && /chat not found|user is deactivated|bot was kicked/i.test(description));\n return { ok: false, error: description, retryable: code >= 500, gone };\n },\n\n async getUpdates({ offset, timeoutSeconds = 25, signal }) {\n // Telegram holds the request open for `timeout` seconds; the budget must outlive it.\n const { data } = await call<TelegramUpdate[]>(\n \"getUpdates\",\n {\n ...(offset === undefined ? {} : { offset }),\n timeout: timeoutSeconds,\n allowed_updates: [\"message\", \"my_chat_member\"],\n },\n signal,\n (timeoutSeconds + 10) * 1000,\n );\n if (!data.ok)\n throw new Error(redact(`telegram getUpdates failed: ${data.description ?? \"unknown\"}`));\n return data.result;\n },\n\n setWebhook: async (url, secretToken) => {\n await must(\"setWebhook\", {\n url,\n secret_token: secretToken,\n allowed_updates: [\"message\", \"my_chat_member\"],\n });\n },\n deleteWebhook: async () => {\n await must(\"deleteWebhook\", {});\n },\n getMe: () => must(\"getMe\", {}),\n };\n}\n"]}
|
package/dist/client.cjs
CHANGED
|
@@ -263,7 +263,12 @@ function createNotifyClient(options = {}) {
|
|
|
263
263
|
headers: { accept: "text/event-stream" },
|
|
264
264
|
signal
|
|
265
265
|
});
|
|
266
|
-
if (!response.ok
|
|
266
|
+
if (!response.ok) throw new Error(`GET /events failed: ${response.status}`);
|
|
267
|
+
if (!response.body) {
|
|
268
|
+
streamDisabled = true;
|
|
269
|
+
schedule(nextDelay());
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
267
272
|
const reader = response.body.getReader();
|
|
268
273
|
const decoder = new TextDecoder();
|
|
269
274
|
let buffer = "";
|
|
@@ -304,7 +309,7 @@ function createNotifyClient(options = {}) {
|
|
|
304
309
|
if (!running) return;
|
|
305
310
|
streamFailures += 1;
|
|
306
311
|
}
|
|
307
|
-
if (!running || isFollower()) return;
|
|
312
|
+
if (!running || isFollower() || streamDisabled) return;
|
|
308
313
|
if (shortStreams >= SHORT_STREAMS_BEFORE_GIVING_UP) {
|
|
309
314
|
streamDisabled = true;
|
|
310
315
|
schedule(nextDelay());
|
package/dist/client.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client/index.ts"],"names":[],"mappings":";;;AAuFA,IAAM,OAAA,GAAuB;AAAA,EAC3B,eAAe,EAAC;AAAA,EAChB,WAAA,EAAa,CAAA;AAAA,EACb,WAAA,EAAa,CAAA;AAAA,EACb,UAAA,EAAY,IAAA;AAAA,EACZ,SAAA,EAAW,IAAA;AAAA,EACX,KAAA,EAAO;AACT,CAAA;AAEA,IAAM,WAAA,GAAc,CAAC,IAAA,KACnB,IAAA,CAAK,OAAO,CAAC,KAAA,EAAO,GAAA,KAAS,GAAA,CAAI,MAAA,KAAW,IAAA,GAAO,KAAA,GAAQ,CAAA,GAAI,OAAQ,CAAC,CAAA;AAG1E,IAAM,eAAA,GAAkB,IAAA;AACxB,IAAM,8BAAA,GAAiC,CAAA;AAQhC,SAAS,kBAAA,CAAmB,OAAA,GAA+B,EAAC,EAAG;AACpE,EAAA,MAAM,WAAW,OAAA,CAAQ,OAAA,IAAW,oBAAA,EAAsB,OAAA,CAAQ,OAAO,EAAE,CAAA;AAC3E,EAAA,MAAM,OAAA,GAAU,QAAQ,KAAA,KAAU,CAAA,GAAI,SAAS,UAAA,CAAW,KAAA,CAAM,GAAG,IAAI,CAAA,CAAA;AACvE,EAAA,MAAM,KAAA,GAAQ,QAAQ,KAAA,IAAS,EAAA;AAC/B,EAAA,MAAM,cAAA,GAAiB,QAAQ,cAAA,IAAkB,IAAA;AACjD,EAAA,MAAM,iBAAA,GAAoB,OAAA,CAAQ,iBAAA,IAAqB,EAAA,GAAK,GAAA;AAC5D,EAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,WAAA,IAAe,CAAA,GAAI,GAAA;AAC/C,EAAA,MAAM,cAAA,GAAiB,QAAQ,cAAA,IAAkB,GAAA;AACjD,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,KAAQ,MAAM,KAAK,GAAA,EAAI,CAAA;AAC3C,EAAA,MAAM,QAAA,GAAW,QAAQ,KAAA,GAAQ,CAAA,EAAG,OAAO,CAAA,CAAA,EAAI,OAAA,CAAQ,KAAK,CAAA,CAAA,GAAK,OAAA;AACjE,EAAA,MAAM,MAAA,GAAS,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,QAAA,KAAa,WAAA;AACpE,EAAA,MAAM,SAAA,GAAuB,QAAQ,SAAA,IAAa,MAAA;AAClD,EAAA,MAAM,WAAA,GAAc,SAAA,KAAc,KAAA,IAAU,SAAA,KAAc,MAAA,IAAU,MAAA;AACpE,EAAA,MAAM,QAAA,GACJ,QAAQ,gBAAA,KACP,MAAM,OAAO,QAAA,KAAa,WAAA,IAAe,SAAS,eAAA,KAAoB,QAAA,CAAA;AAEzE,EAAA,MAAM,KAAA,GACJ,OAAA,CAAQ,KAAA,KAAU,MAAA,GACd,OAAO,SAAA,KAAc,WAAA,GACjB,SAAA,CAA0C,KAAA,IAAS,IAAA,GACrD,IAAA,GACF,OAAA,CAAQ,KAAA;AACd,EAAA,MAAM,WAAA,GACJ,OAAA,CAAQ,OAAA,KAAY,MAAA,GAChB,OAAO,gBAAA,KAAqB,WAAA,GAC1B,MAAM,IAAI,iBAAiB,CAAA,UAAA,EAAa,QAAQ,CAAA,CAAE,CAAA,GAClD,OACF,OAAA,CAAQ,OAAA;AACd,EAAA,MAAM,aAAA,GACJ,OAAA,CAAQ,aAAA,KAAkB,MAAA,GACtB,OAAO,SAAA,KAAc,WAAA,GACjB,SAAA,CAAoD,aAAA,IAAiB,IAAA,GACvE,IAAA,GACF,OAAA,CAAQ,aAAA;AACd,EAAA,MAAM,iBACJ,OAAA,CAAQ,cAAA,KAAmB,SACvB,MAAA,GACG,MAAA,GACD,OACF,OAAA,CAAQ,cAAA;AAEd,EAAA,IAAI,KAAA,GAAqB,OAAA;AACzB,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAkC;AAExD,EAAA,IAAI,IAAA,GAAgC,MAAA;AACpC,EAAA,IAAI,OAAA,GAAU,KAAA;AACd,EAAA,IAAI,OAAA;AACJ,EAAA,IAAI,WAAA;AACJ,EAAA,IAAI,SAAA;AAEJ,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI,mBAAA,GAAsB,CAAA;AAC1B,EAAA,IAAI,SAAA,GAAY,CAAA;AAChB,EAAA,IAAI,WAAA,GAAc,CAAA;AAClB,EAAA,IAAI,aAAA,GAAgB,CAAA;AACpB,EAAA,IAAI,UAAA;AACJ,EAAA,IAAI,YAAA,GAAe,KAAA;AAEnB,EAAA,IAAI,WAAA;AACJ,EAAA,IAAI,eAAA,GAAkB,KAAA;AACtB,EAAA,IAAI,iBAAiB,CAAC,WAAA;AACtB,EAAA,IAAI,YAAA,GAAe,CAAA;AACnB,EAAA,IAAI,cAAA,GAAiB,CAAA;AAGrB,EAAA,IAAI,UAAA,GAAa,CAAA;AAGjB,EAAA,MAAM,UAAA,uBAAiB,GAAA,EAAY;AACnC,EAAA,IAAI,SAAA,GAAY,KAAA;AAEhB,EAAA,MAAM,KAAA,GAAQ,MAAA,iBAAM,IAAI,IAAA,IAAO,WAAA,EAAY;AAE3C,EAAA,SAAS,IAAA,GAAO;AACd,IAAA,KAAA,MAAW,QAAA,IAAY,SAAA,EAAW,QAAA,CAAS,KAAK,CAAA;AAAA,EAClD;AAEA,EAAA,SAAS,IAAI,KAAA,EAA6B;AACxC,IAAA,KAAA,GAAQ,EAAE,GAAG,KAAA,EAAO,GAAG,KAAA,EAAM;AAC7B,IAAA,KAAA,GAAQ,EAAE,GAAG,KAAA,EAAO,aAAa,WAAA,CAAY,KAAA,CAAM,aAAa,CAAA,EAAE;AAClE,IAAA,IAAA,EAAK;AACL,IAAA,IAAI,SAAS,QAAA,EAAU,IAAA,CAAK,EAAE,IAAA,EAAM,OAAA,EAAS,OAAO,CAAA;AAAA,EACtD;AAEA,EAAA,SAAS,KAAK,OAAA,EAAqB;AACjC,IAAA,IAAI;AACF,MAAA,OAAA,EAAS,YAAY,OAAO,CAAA;AAAA,IAC9B,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AAEA,EAAA,eAAe,OAAA,CAAW,MAAc,IAAA,EAAgC;AACtE,IAAA,MAAM,WAAW,MAAM,OAAA,CAAQ,GAAG,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI;AAAA,MAClD,GAAG,IAAA;AAAA,MACH,SAAS,EAAE,cAAA,EAAgB,kBAAA,EAAoB,GAAG,MAAM,OAAA;AAAQ,KACjE,CAAA;AACD,IAAA,IAAI,CAAC,QAAA,CAAS,EAAA;AACZ,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,EAAG,IAAA,EAAM,MAAA,IAAU,KAAK,CAAA,CAAA,EAAI,IAAI,CAAA,SAAA,EAAY,QAAA,CAAS,MAAM,CAAA,CAAE,CAAA;AAC/E,IAAA,OAAQ,MAAM,SAAS,IAAA,EAAK;AAAA,EAC9B;AAEA,EAAA,eAAe,SAAA,GAA2B;AACxC,IAAA,MAAM,SAAA,GAAY,UAAA;AAElB,IAAA,MAAM,IAAA,GAAO,MAAM,OAAA,CAIhB,CAAA,QAAA,EAAW,KAAK,CAAA,CAAE,CAAA;AAErB,IAAA,MAAM,cAAc,IAAA,CAAK,WAAA,IAAA,CAAgB,MAAM,OAAA,CAA4B,QAAQ,CAAA,EAAG,MAAA;AAGtF,IAAA,IAAI,eAAe,SAAA,EAAW;AAE9B,IAAA,GAAA,CAAI;AAAA,MACF,eAAe,IAAA,CAAK,aAAA;AAAA,MACpB,YAAY,IAAA,CAAK,UAAA;AAAA,MACjB,WAAA;AAAA,MACA,SAAA,EAAW,KAAA;AAAA,MACX,KAAA,EAAO;AAAA,KACR,CAAA;AAAA,EACH;AAGA,EAAA,SAAS,OAAA,GAAyB;AAChC,IAAA,IAAI,UAAA,EAAY;AACd,MAAA,YAAA,GAAe,IAAA;AACf,MAAA,OAAO,UAAA;AAAA,IACT;AACA,IAAA,UAAA,GAAA,CAAc,YAAY;AACxB,MAAA,IAAI;AACF,QAAA,GAAG;AACD,UAAA,YAAA,GAAe,KAAA;AACf,UAAA,MAAM,SAAA,EAAU;AAChB,UAAA,aAAA,GAAgB,GAAA,EAAI;AAAA,QACtB,CAAA,QAAS,YAAA;AAAA,MACX,CAAA,SAAE;AACA,QAAA,UAAA,GAAa,MAAA;AAAA,MACf;AAAA,IACF,CAAA,GAAG;AACH,IAAA,OAAO,UAAA;AAAA,EACT;AAEA,EAAA,IAAI,WAAA;AAEJ,EAAA,eAAe,QAAA,GAA0B;AACvC,IAAA,IAAI,CAAC,MAAM,UAAA,EAAY;AAGvB,IAAA,IAAI,aAAa,OAAO,WAAA;AAExB,IAAA,MAAM,SAAS,KAAA,CAAM,UAAA;AAErB,IAAA,WAAA,GAAA,CAAe,YAAY;AACzB,MAAA,IAAI;AACF,QAAA,MAAM,IAAA,GAAO,MAAM,OAAA,CAGhB,CAAA,QAAA,EAAW,KAAK,CAAA,QAAA,EAAW,kBAAA,CAAmB,MAAM,CAAC,CAAA,CAAE,CAAA;AAE1D,QAAA,MAAM,KAAA,GAAQ,IAAI,GAAA,CAAI,KAAA,CAAM,aAAA,CAAc,IAAI,CAAC,GAAA,KAAQ,GAAA,CAAI,EAAE,CAAC,CAAA;AAE9D,QAAA,GAAA,CAAI;AAAA,UACF,aAAA,EAAe;AAAA,YACb,GAAG,KAAA,CAAM,aAAA;AAAA,YACT,GAAG,IAAA,CAAK,aAAA,CAAc,MAAA,CAAO,CAAC,GAAA,KAAQ,CAAC,KAAA,CAAM,GAAA,CAAI,GAAA,CAAI,EAAE,CAAC;AAAA,WAC1D;AAAA,UACA,YAAY,IAAA,CAAK;AAAA,SAClB,CAAA;AAAA,MACH,CAAA,SAAE;AACA,QAAA,WAAA,GAAc,MAAA;AAAA,MAChB;AAAA,IACF,CAAA,GAAG;AAEH,IAAA,OAAO,WAAA;AAAA,EACT;AAGA,EAAA,eAAe,UAAA,CACb,KAAA,EACA,KAAA,EACA,IAAA,EACe;AACf,IAAA,MAAM,QAAA,GAAW,KAAA;AACjB,IAAA,UAAA,IAAc,CAAA;AACd,IAAA,GAAA,CAAI,EAAE,eAAe,KAAA,CAAM,KAAA,CAAM,aAAa,CAAA,EAAG,GAAG,OAAO,CAAA;AAE3D,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,EAAK;AACX,MAAA,IAAI,SAAS,UAAA,EAAY,IAAA,CAAK,EAAE,IAAA,EAAM,WAAW,CAAA;AAAA,IACnD,SAAS,KAAA,EAAO;AACd,MAAA,UAAA,IAAc,CAAA;AACd,MAAA,KAAA,GAAQ,QAAA;AACR,MAAA,IAAA,EAAK;AACL,MAAA,MAAM,KAAA;AAAA,IACR;AAAA,EACF;AAEA,EAAA,eAAe,WAAW,EAAA,EAA2B;AACnD,IAAA,IAAI,IAAA,KAAS,UAAA,EAAY,UAAA,CAAW,GAAA,CAAI,EAAE,CAAA;AAC1C,IAAA,MAAM,UAAA;AAAA,MACJ,CAAC,IAAA,KACC,IAAA,CAAK,IAAI,CAAC,GAAA,KAAS,IAAI,EAAA,KAAO,EAAA,GAAK,EAAE,GAAG,KAAK,MAAA,EAAQ,GAAA,CAAI,UAAU,KAAA,EAAM,KAAM,GAAI,CAAA;AAAA,MACrF,EAAC;AAAA,MACD,MAAM,OAAA,CAAQ,OAAA,EAAS,EAAE,MAAA,EAAQ,QAAQ,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,EAAE,KAAK,CAAC,EAAE,CAAA,EAAG,GAAG;AAAA,KAChF,CAAE,KAAA,CAAM,CAAC,KAAA,KAAU;AACjB,MAAA,UAAA,CAAW,OAAO,EAAE,CAAA;AACpB,MAAA,MAAM,KAAA;AAAA,IACR,CAAC,CAAA;AAAA,EACH;AAEA,EAAA,eAAe,WAAA,GAA6B;AAC1C,IAAA,IAAI,IAAA,KAAS,YAAY,KAAA,MAAW,GAAA,IAAO,MAAM,aAAA,EAAe,UAAA,CAAW,GAAA,CAAI,GAAA,CAAI,EAAE,CAAA;AACrF,IAAA,MAAM,UAAA;AAAA,MACJ,CAAC,IAAA,KAAS,IAAA,CAAK,GAAA,CAAI,CAAC,GAAA,MAAS,EAAE,GAAG,GAAA,EAAK,MAAA,EAAQ,GAAA,CAAI,MAAA,IAAU,KAAA,IAAQ,CAAE,CAAA;AAAA,MACvE,EAAC;AAAA,MACD,MAAM,OAAA,CAAQ,WAAA,EAAa,EAAE,MAAA,EAAQ,QAAQ;AAAA,KAC/C,CAAE,KAAA,CAAM,CAAC,KAAA,KAAU;AACjB,MAAA,UAAA,CAAW,KAAA,EAAM;AACjB,MAAA,MAAM,KAAA;AAAA,IACR,CAAC,CAAA;AAAA,EACH;AAEA,EAAA,eAAe,QAAA,GAA0B;AAEvC,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,aAAA,CAAc,CAAC,CAAA,EAAG,SAAA;AACvC,IAAA,IAAI,IAAA,KAAS,YAAY,SAAA,GAAY,IAAA;AAErC,IAAA,MAAM,UAAA;AAAA,MACJ,CAAC,IAAA,KAAS,IAAA,CAAK,GAAA,CAAI,CAAC,GAAA,MAAS,EAAE,GAAG,GAAA,EAAK,MAAA,EAAQ,GAAA,CAAI,MAAA,IAAU,KAAA,IAAQ,CAAE,CAAA;AAAA,MACvE,EAAE,aAAa,CAAA,EAAE;AAAA,MACjB,MACE,QAAQ,OAAA,EAAS;AAAA,QACf,MAAA,EAAQ,MAAA;AAAA,QACR,IAAA,EAAM,KAAK,SAAA,CAAU,MAAA,GAAS,EAAE,MAAA,EAAQ,MAAA,EAAO,GAAI,EAAE;AAAA,OACtD;AAAA,KACL,CAAE,KAAA,CAAM,CAAC,KAAA,KAAU;AACjB,MAAA,SAAA,GAAY,KAAA;AACZ,MAAA,MAAM,KAAA;AAAA,IACR,CAAC,CAAA;AAAA,EACH;AAIA,EAAA,SAAS,iBAAiB,MAAA,EAAqB;AAC7C,IAAA,MAAM,aAAA,GAAgB,MAAA,CAAO,aAAA,CAAc,GAAA,CAAI,CAAC,GAAA,KAAQ;AACtD,MAAA,IAAI,CAAC,UAAA,CAAW,GAAA,CAAI,GAAA,CAAI,EAAE,GAAG,OAAO,GAAA;AACpC,MAAA,IAAI,IAAI,MAAA,EAAQ;AACd,QAAA,UAAA,CAAW,MAAA,CAAO,IAAI,EAAE,CAAA;AACxB,QAAA,OAAO,GAAA;AAAA,MACT;AACA,MAAA,OAAO,EAAE,GAAG,GAAA,EAAK,MAAA,EAAQ,OAAM,EAAE;AAAA,IACnC,CAAC,CAAA;AACD,IAAA,IAAI,SAAA,IAAa,MAAA,CAAO,WAAA,KAAgB,CAAA,EAAG,SAAA,GAAY,KAAA;AACvD,IAAA,KAAA,GAAQ;AAAA,MACN,GAAG,MAAA;AAAA,MACH,aAAA;AAAA,MACA,WAAA,EAAa,SAAA,GAAY,CAAA,GAAI,MAAA,CAAO,WAAA;AAAA,MACpC,WAAA,EAAa,YAAY,aAAa;AAAA,KACxC;AACA,IAAA,IAAA,EAAK;AAAA,EACP;AAEA,EAAA,SAAS,aAAa,KAAA,EAA0B;AAC9C,IAAA,MAAM,UAAU,KAAA,CAAM,IAAA;AACtB,IAAA,IAAI,CAAC,OAAA,IAAW,OAAO,OAAA,KAAY,QAAA,EAAU;AAC7C,IAAA,IAAI,SAAS,QAAA,EAAU;AACrB,MAAA,IAAI,OAAA,CAAQ,SAAS,OAAA,EAAS,IAAA,CAAK,EAAE,IAAA,EAAM,OAAA,EAAS,OAAO,CAAA;AAAA,WAAA,IAClD,QAAQ,IAAA,KAAS,SAAA,OAAgB,OAAA,EAAQ,CAAE,MAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AAClE,MAAA;AAAA,IACF;AACA,IAAA,IAAI,OAAA,CAAQ,IAAA,KAAS,OAAA,EAAS,gBAAA,CAAiB,QAAQ,KAAK,CAAA;AAAA,EAC9D;AAIA,EAAA,SAAS,SAAS,KAAA,EAAe;AAC/B,IAAA,YAAA,CAAa,KAAK,CAAA;AAClB,IAAA,KAAA,GAAQ,UAAA,CAAW,MAAM,KAAK,CAAA;AAAA,EAChC;AAEA,EAAA,SAAS,SAAA,GAAoB;AAC3B,IAAA,IAAI,sBAAsB,CAAA,EAAG;AAC3B,MAAA,OAAO,IAAA,CAAK,GAAA,CAAI,cAAA,GAAiB,CAAA,IAAK,qBAAqB,iBAAiB,CAAA;AAAA,IAC9E;AACA,IAAA,IAAI,iBAAiB,OAAO,WAAA;AAC5B,IAAA,IAAI,GAAA,EAAI,GAAI,WAAA,EAAa,OAAO,cAAA;AAChC,IAAA,SAAA,IAAa,CAAA;AACb,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,cAAA,GAAiB,CAAA,IAAK,WAAW,iBAAiB,CAAA;AAAA,EACpE;AAEA,EAAA,eAAe,IAAA,GAAO;AACpB,IAAA,IAAI,CAAC,OAAA,EAAS;AAEd,IAAA,IAAI,UAAS,EAAG;AACd,MAAA,QAAA,CAAS,cAAc,CAAA;AACvB,MAAA;AAAA,IACF;AAEA,IAAA,IAAI;AACF,MAAA,MAAM,OAAA,EAAQ;AACd,MAAA,mBAAA,GAAsB,CAAA;AAAA,IACxB,SAAS,KAAA,EAAO;AACd,MAAA,mBAAA,IAAuB,CAAA;AACvB,MAAA,GAAA,CAAI,EAAE,SAAA,EAAW,KAAA,EAAO,KAAA,EAAO,KAAA,YAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,KAAA,CAAM,MAAA,CAAO,KAAK,CAAC,GAAG,CAAA;AAAA,IAC5F;AAEA,IAAA,IAAI,OAAA,EAAS,QAAA,CAAS,SAAA,EAAW,CAAA;AAAA,EACnC;AAGA,EAAA,SAAS,UAAA,GAAa;AACpB,IAAA,IAAI,CAAC,OAAA,IAAW,IAAA,KAAS,UAAA,EAAY;AACrC,IAAA,WAAA,GAAc,KAAI,GAAI,cAAA;AACtB,IAAA,SAAA,GAAY,CAAA;AACZ,IAAA,IAAI,CAAC,QAAA,EAAS,IAAK,GAAA,EAAI,GAAI,gBAAgB,cAAA,EAAgB;AACzD,MAAA,KAAK,OAAA,EAAQ,CAAE,KAAA,CAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AAC7B,MAAA,QAAA,CAAS,WAAW,CAAA;AAAA,IACtB;AAAA,EACF;AAEA,EAAA,MAAM,eAAe,MAAM;AACzB,IAAA,IAAI,CAAC,QAAA,EAAS,EAAG,UAAA,EAAW;AAAA,EAC9B,CAAA;AAEA,EAAA,MAAM,eAAA,GAAkB,CAAC,OAAA,EAAS,aAAA,EAAe,WAAW,QAAQ,CAAA;AAIpE,EAAA,SAAS,OAAA,GAAU;AACjB,IAAA,IAAI,CAAC,OAAA,EAAS;AACd,IAAA,KAAK,OAAA,EAAQ,CAAE,KAAA,CAAM,MAAM;AAAA,IAAC,CAAC,CAAA;AAAA,EAC/B;AAEA,EAAA,eAAe,WAAW,MAAA,EAAoC;AAC5D,IAAA,MAAM,WAAW,GAAA,EAAI;AACrB,IAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,CAAA,EAAG,OAAO,CAAA,OAAA,CAAA,EAAW;AAAA,MAClD,OAAA,EAAS,EAAE,MAAA,EAAQ,mBAAA,EAAoB;AAAA,MACvC;AAAA,KACD,CAAA;AACD,IAAA,IAAI,CAAC,QAAA,CAAS,EAAA,IAAM,CAAC,QAAA,CAAS,IAAA,EAAM,MAAM,IAAI,KAAA,CAAM,CAAA,oBAAA,EAAuB,QAAA,CAAS,MAAM,CAAA,CAAE,CAAA;AAE5F,IAAA,MAAM,MAAA,GAAS,QAAA,CAAS,IAAA,CAAK,SAAA,EAAU;AACvC,IAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAChC,IAAA,IAAI,MAAA,GAAS,EAAA;AACb,IAAA,IAAI,SAAA,GAAY,EAAA;AAEhB,IAAA,IAAI;AACF,MAAA,WAAS;AACP,QAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,MAAM,OAAO,IAAA,EAAK;AAC1C,QAAA,IAAI,IAAA,EAAM;AACV,QAAA,MAAA,IAAU,QAAQ,MAAA,CAAO,KAAA,EAAO,EAAE,MAAA,EAAQ,MAAM,CAAA;AAChD,QAAA,IAAI,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAA;AACjC,QAAA,OAAO,WAAW,CAAA,EAAG;AACnB,UAAA,MAAM,IAAA,GAAO,OAAO,KAAA,CAAM,CAAA,EAAG,OAAO,CAAA,CAAE,OAAA,CAAQ,OAAO,EAAE,CAAA;AACvD,UAAA,MAAA,GAAS,MAAA,CAAO,KAAA,CAAM,OAAA,GAAU,CAAC,CAAA;AACjC,UAAA,IAAI,SAAS,EAAA,EAAI;AACf,YAAA,IAAI,cAAc,OAAA,EAAS;AACzB,cAAA,eAAA,GAAkB,IAAA;AAClB,cAAA,cAAA,GAAiB,CAAA;AACjB,cAAA,QAAA,CAAS,WAAW,CAAA;AAAA,YACtB,CAAA,MAAA,IAAW,SAAA,KAAc,SAAA,EAAW,OAAA,EAAQ;AAC5C,YAAA,SAAA,GAAY,EAAA;AAAA,UACd,CAAA,MAAA,IAAW,IAAA,CAAK,UAAA,CAAW,QAAQ,CAAA,cAAe,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA,CAAE,IAAA,EAAK;AACrE,UAAA,OAAA,GAAU,MAAA,CAAO,QAAQ,IAAI,CAAA;AAAA,QAC/B;AAAA,MACF;AAAA,IACF,CAAA,SAAE;AACA,MAAA,eAAA,GAAkB,KAAA;AAClB,MAAA,IAAI,GAAA,EAAI,GAAI,QAAA,GAAW,eAAA,EAAiB,YAAA,IAAgB,CAAA;AAAA,WACnD,YAAA,GAAe,CAAA;AAAA,IACtB;AAAA,EACF;AAEA,EAAA,MAAM,UAAA,GAAa,MAAM,IAAA,KAAS,UAAA;AAElC,EAAA,eAAe,UAAA,GAAa;AAC1B,IAAA,OAAO,OAAA,IAAW,CAAC,cAAA,IAAkB,CAAC,YAAW,EAAG;AAClD,MAAA,WAAA,GAAc,IAAI,eAAA,EAAgB;AAClC,MAAA,IAAI;AACF,QAAA,MAAM,UAAA,CAAW,YAAY,MAAM,CAAA;AAAA,MACrC,CAAA,CAAA,MAAQ;AACN,QAAA,IAAI,CAAC,OAAA,EAAS;AACd,QAAA,cAAA,IAAkB,CAAA;AAAA,MACpB;AACA,MAAA,IAAI,CAAC,OAAA,IAAW,UAAA,EAAW,EAAG;AAC9B,MAAA,IAAI,gBAAgB,8BAAA,EAAgC;AAElD,QAAA,cAAA,GAAiB,IAAA;AACjB,QAAA,QAAA,CAAS,WAAW,CAAA;AACpB,QAAA;AAAA,MACF;AACA,MAAA,MAAM,IAAI,OAAA;AAAA,QAAQ,CAAC,OAAA,KACjB,UAAA,CAAW,OAAA,EAAS,IAAA,CAAK,IAAI,GAAA,GAAQ,CAAA,IAAK,cAAA,EAAgB,GAAM,CAAC;AAAA,OACnE;AAAA,IACF;AAAA,EACF;AAIA,EAAA,SAAS,YAAA,GAAe;AACtB,IAAA,IAAA,GAAO,QAAA;AACP,IAAA,IAAA,CAAK,EAAE,IAAA,EAAM,QAAA,EAAU,CAAA;AACvB,IAAA,IAAA,CAAK,EAAE,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,CAAA;AAC7B,IAAA,KAAK,IAAA,EAAK;AACV,IAAA,IAAI,CAAC,cAAA,EAAgB,KAAK,UAAA,EAAW;AAAA,EACvC;AAEA,EAAA,SAAS,cAAA,GAAiB;AACxB,IAAA,IAAA,GAAO,UAAA;AACP,IAAA,IAAA,CAAK,EAAE,IAAA,EAAM,OAAA,EAAS,CAAA;AAAA,EACxB;AAEA,EAAA,SAAS,iBAAA,GAAoB;AAC3B,IAAA,IAAI,CAAC,KAAA,IAAS,CAAC,OAAA,EAAS;AACtB,MAAA,IAAA,GAAO,MAAA;AACP,MAAA,KAAK,IAAA,EAAK;AACV,MAAA,IAAI,CAAC,cAAA,EAAgB,KAAK,UAAA,EAAW;AACrC,MAAA;AAAA,IACF;AACA,IAAA,cAAA,EAAe;AACf,IAAA,SAAA,GAAY,IAAI,eAAA,EAAgB;AAChC,IAAA,KAAA,CACG,OAAA;AAAA,MACC,oBAAoB,QAAQ,CAAA,CAAA;AAAA,MAC5B,EAAE,IAAA,EAAM,WAAA,EAAa,MAAA,EAAQ,UAAU,MAAA,EAAO;AAAA,MAC9C,MACE,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AAC7B,QAAA,WAAA,GAAc,OAAA;AACd,QAAA,IAAI,SAAS,YAAA,EAAa;AAAA,aACrB,OAAA,EAAQ;AAAA,MACf,CAAC;AAAA,KACL,CACC,MAAM,MAAM;AAAA,IAEb,CAAC,CAAA;AAAA,EACL;AAEA,EAAA,SAAS,KAAA,GAAQ;AACf,IAAA,OAAA,GAAU,IAAA;AACV,IAAA,OAAA,GAAU,WAAA,GAAc,aAAY,GAAI,MAAA;AACxC,IAAA,OAAA,EAAS,gBAAA,CAAiB,WAAW,YAAY,CAAA;AACjD,IAAA,aAAA,EAAe,gBAAA,CAAiB,WAAW,OAAO,CAAA;AAClD,IAAA,IAAI,cAAA,EAAgB;AAClB,MAAA,KAAA,MAAW,IAAA,IAAQ,eAAA,EAAiB,cAAA,CAAe,gBAAA,CAAiB,MAAM,UAAU,CAAA;AAAA,IACtF;AACA,IAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACnC,MAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAoB,YAAY,CAAA;AAAA,IAC5D;AACA,IAAA,WAAA,GAAc,KAAI,GAAI,cAAA;AACtB,IAAA,iBAAA,EAAkB;AAAA,EACpB;AAEA,EAAA,SAAS,IAAA,GAAO;AACd,IAAA,OAAA,GAAU,KAAA;AACV,IAAA,YAAA,CAAa,KAAK,CAAA;AAClB,IAAA,KAAA,GAAQ,MAAA;AACR,IAAA,WAAA,EAAa,KAAA,EAAM;AACnB,IAAA,WAAA,GAAc,MAAA;AACd,IAAA,SAAA,EAAW,KAAA,EAAM;AACjB,IAAA,SAAA,GAAY,MAAA;AACZ,IAAA,WAAA,IAAc;AACd,IAAA,WAAA,GAAc,MAAA;AACd,IAAA,OAAA,EAAS,mBAAA,CAAoB,WAAW,YAAY,CAAA;AACpD,IAAA,OAAA,EAAS,KAAA,EAAM;AACf,IAAA,OAAA,GAAU,MAAA;AACV,IAAA,aAAA,EAAe,mBAAA,CAAoB,WAAW,OAAO,CAAA;AACrD,IAAA,IAAI,cAAA,EAAgB;AAClB,MAAA,KAAA,MAAW,IAAA,IAAQ,eAAA,EAAiB,cAAA,CAAe,mBAAA,CAAoB,MAAM,UAAU,CAAA;AAAA,IACzF;AACA,IAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACnC,MAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAoB,YAAY,CAAA;AAAA,IAC/D;AACA,IAAA,IAAA,GAAO,MAAA;AAAA,EACT;AAIA,EAAA,SAAS,QAAQ,KAAA,EAA2B;AAC1C,IAAA,MAAM,OAAO,KAAA,CAAM,IAAA;AACnB,IAAA,IAAI,MAAM,MAAA,KAAW,WAAA,IAAe,IAAA,CAAK,IAAA,KAAS,WAAW,OAAA,EAAQ;AAAA,EACvE;AAEA,EAAA,IAAI,gBAAA,GAAkC,IAAA;AAGtC,EAAA,MAAM,UAAA,GAAa,CAAC,QAAA,KAAuB;AACzC,IAAA,IAAI,CAAC,QAAA,CAAS,GAAA,IAAO,OAAO,QAAA,KAAa,aAAa,OAAO,IAAA;AAC7D,IAAA,IAAI;AACF,MAAA,OAAO,IAAI,GAAA,CAAI,QAAA,CAAS,GAAG,CAAA,CAAE,WAAW,QAAA,CAAS,MAAA;AAAA,IACnD,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF,CAAA;AAOA,EAAA,SAAS,WAAW,KAAA,EAAyD;AAC3E,IAAA,QAAQ,UAAU,IAAA,KAA8C;AAC9D,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,IAAI,CAAA;AACpC,MAAA,MAAM,OAAA,GAAU,WAAW,QAAQ,CAAA,GAAI,SAAS,OAAA,CAAQ,GAAA,CAAI,mBAAmB,CAAA,GAAI,IAAA;AACnF,MAAA,IAAI,OAAA,KAAY,IAAA,IAAQ,OAAA,KAAY,gBAAA,EAAkB;AACpD,QAAA,MAAM,QAAQ,gBAAA,KAAqB,IAAA;AACnC,QAAA,gBAAA,GAAmB,OAAA;AACnB,QAAA,IAAI,CAAC,KAAA,IAAS,IAAA,KAAS,UAAA,EAAY,OAAA,EAAQ;AAAA,MAC7C;AACA,MAAA,OAAO,QAAA;AAAA,IACT,CAAA;AAAA,EACF;AAEA,EAAA,SAAS,UAAU,QAAA,EAAoD;AACrE,IAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,IAAA,QAAA,CAAS,KAAK,CAAA;AAGd,IAAA,IAAI,SAAA,CAAU,IAAA,KAAS,CAAA,EAAG,KAAA,EAAM;AAEhC,IAAA,OAAO,MAAM;AACX,MAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AACzB,MAAA,IAAI,SAAA,CAAU,IAAA,KAAS,CAAA,EAAG,IAAA,EAAK;AAAA,IACjC,CAAA;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,SAAA;AAAA,IACA,OAAA;AAAA,IACA,QAAA;AAAA,IACA,UAAA;AAAA,IACA,WAAA;AAAA,IACA,QAAA;AAAA,IACA,UAAA;AAAA,IACA,UAAU,MAAM,KAAA;AAAA,IAChB,cAAc,OAAwB;AAAA,MACpC,IAAA;AAAA,MACA,SAAA,EAAW,iBAAiB,MAAA,GAAS,KAAA;AAAA,MACrC,SAAA,EAAW;AAAA,KACb;AAAA,GACF;AACF","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 Transport = \"auto\" | \"sse\" | \"poll\";\n\nexport type TransportStatus = {\n /** \"leader\" holds this browser's single connection; \"follower\" mirrors it; \"solo\" has no Web Locks. */\n role: \"leader\" | \"follower\" | \"solo\";\n /** What the leader is using right now. */\n transport: \"sse\" | \"poll\";\n /** True while the event stream has said `ready` and not since closed. */\n connected: boolean;\n};\n\ntype LockManagerLike = {\n request(\n name: string,\n options: { mode: \"exclusive\"; signal?: AbortSignal },\n callback: () => Promise<void>,\n ): Promise<void>;\n};\n\ntype ChannelLike = {\n postMessage(message: unknown): void;\n addEventListener(type: \"message\", handler: (event: { data: unknown }) => void): void;\n removeEventListener(type: \"message\", handler: (event: { data: unknown }) => void): void;\n close(): void;\n};\n\ntype MessageTargetLike = {\n addEventListener(type: string, handler: (event: { data?: unknown }) => void): void;\n removeEventListener(type: string, handler: (event: { data?: unknown }) => void): void;\n};\n\nexport type NotifyClientOptions = {\n baseUrl?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n limit?: number | undefined;\n /** Poll interval while the user is active and no stream is connected. Default 15 s. */\n pollIntervalMs?: number | undefined;\n /** Ceiling the idle backoff grows to. Default 10 min. */\n maxPollIntervalMs?: number | undefined;\n /** Poll interval while the event stream is connected: a safety net. Default 5 min. */\n safetyNetMs?: number | undefined;\n /** How long after user input polling stays at the base interval. Default 60 s. */\n activeWindowMs?: number | undefined;\n /** \"auto\" streams when a DOM is present, \"poll\" never streams, \"sse\" always tries. */\n transport?: Transport | undefined;\n /** Injectable so the poller can be tested without a DOM. */\n isDocumentHidden?: (() => boolean) | undefined;\n /** Web Locks, for one connection per browser. Defaults to navigator.locks; null disables. */\n locks?: LockManagerLike | null | undefined;\n /** Cross-tab channel factory. Defaults to BroadcastChannel; null disables. */\n channel?: (() => ChannelLike) | null | undefined;\n /** Where push relays arrive. Defaults to navigator.serviceWorker; null disables. */\n serviceWorker?: MessageTargetLike | null | undefined;\n /** Where user activity is observed. Defaults to window; null disables. */\n activityTarget?: MessageTargetLike | null | undefined;\n now?: (() => number) | undefined;\n /**\n * Separates browsers' tab groups when identity is not a cookie: pass the\n * current user's id (or session id) so tabs signed in as different users\n * never share a leader or mirror each other's inbox.\n */\n scope?: string | 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\n/** A stream that dies this fast, repeatedly, is being cut by the platform: stop trying. */\nconst SHORT_STREAM_MS = 15_000;\nconst SHORT_STREAMS_BEFORE_GIVING_UP = 3;\n\ntype TabMessage =\n | { type: \"hello\" }\n | { type: \"state\"; state: NotifyState }\n | { type: \"mutated\" }\n | { type: \"leader\" };\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 ?? 10 * 60_000;\n const safetyNetMs = options.safetyNetMs ?? 5 * 60_000;\n const activeWindowMs = options.activeWindowMs ?? 60_000;\n const now = options.now ?? (() => Date.now());\n const groupKey = options.scope ? `${baseUrl}#${options.scope}` : baseUrl;\n const hasDom = typeof window !== \"undefined\" && typeof document !== \"undefined\";\n const transport: Transport = options.transport ?? \"auto\";\n const wantsStream = transport === \"sse\" || (transport === \"auto\" && hasDom);\n const isHidden =\n options.isDocumentHidden ??\n (() => typeof document !== \"undefined\" && document.visibilityState === \"hidden\");\n\n const locks =\n options.locks === undefined\n ? typeof navigator !== \"undefined\"\n ? ((navigator as { locks?: LockManagerLike }).locks ?? null)\n : null\n : options.locks;\n const makeChannel =\n options.channel === undefined\n ? typeof BroadcastChannel !== \"undefined\"\n ? () => new BroadcastChannel(`easy-ping:${groupKey}`) as unknown as ChannelLike\n : null\n : options.channel;\n const serviceWorker =\n options.serviceWorker === undefined\n ? typeof navigator !== \"undefined\"\n ? ((navigator as { serviceWorker?: MessageTargetLike }).serviceWorker ?? null)\n : null\n : options.serviceWorker;\n const activityTarget =\n options.activityTarget === undefined\n ? hasDom\n ? (window as unknown as MessageTargetLike)\n : null\n : options.activityTarget;\n\n let state: NotifyState = INITIAL;\n const listeners = new Set<(state: NotifyState) => void>();\n\n let role: TransportStatus[\"role\"] = \"solo\";\n let running = false;\n let channel: ChannelLike | undefined;\n let releaseLock: (() => void) | undefined;\n let lockAbort: AbortController | undefined;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let consecutiveFailures = 0;\n let idleTicks = 0;\n let activeUntil = 0;\n let lastRefreshAt = 0;\n let refreshing: Promise<void> | undefined;\n let refreshAgain = false;\n\n let streamAbort: AbortController | undefined;\n let streamConnected = false;\n let streamDisabled = !wantsStream;\n let shortStreams = 0;\n let streamFailures = 0;\n\n // Bumped per mutation so an in-flight poll cannot revert an optimistic update.\n let generation = 0;\n\n // A follower's own optimistic changes, kept until the leader's state reflects them.\n const localReads = new Set<string>();\n let localSeen = false;\n\n const stamp = () => new Date().toISOString();\n\n function emit() {\n for (const listener of listeners) listener(state);\n }\n\n function set(patch: Partial<NotifyState>) {\n state = { ...state, ...patch };\n state = { ...state, unreadCount: countUnread(state.notifications) };\n emit();\n if (role === \"leader\") post({ type: \"state\", state });\n }\n\n function post(message: TabMessage) {\n try {\n channel?.postMessage(message);\n } catch {\n // channel closed\n }\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 fetchPage(): Promise<void> {\n const startedAt = generation;\n\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 /** Coalesces overlapping calls: a second request while one is in flight runs once after it. */\n function refresh(): Promise<void> {\n if (refreshing) {\n refreshAgain = true;\n return refreshing;\n }\n refreshing = (async () => {\n try {\n do {\n refreshAgain = false;\n await fetchPage();\n lastRefreshAt = now();\n } while (refreshAgain);\n } finally {\n refreshing = undefined;\n }\n })();\n return refreshing;\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 if (role === \"follower\") post({ type: \"mutated\" });\n } catch (error) {\n generation += 1;\n state = previous;\n emit();\n throw error;\n }\n }\n\n async function markAsRead(id: string): Promise<void> {\n if (role === \"follower\") localReads.add(id);\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 ).catch((error) => {\n localReads.delete(id);\n throw error;\n });\n }\n\n async function markAllRead(): Promise<void> {\n if (role === \"follower\") for (const row of state.notifications) localReads.add(row.id);\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, readAt: row.readAt ?? stamp() })),\n {},\n () => request(\"/read-all\", { method: \"POST\" }),\n ).catch((error) => {\n localReads.clear();\n throw error;\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 if (role === \"follower\") localSeen = true;\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 ).catch((error) => {\n localSeen = false;\n throw error;\n });\n }\n\n // ---- follower: mirror the leader, keep own optimistic changes until confirmed ----\n\n function applyRemoteState(remote: NotifyState) {\n const notifications = remote.notifications.map((row) => {\n if (!localReads.has(row.id)) return row;\n if (row.readAt) {\n localReads.delete(row.id);\n return row;\n }\n return { ...row, readAt: stamp() };\n });\n if (localSeen && remote.unseenCount === 0) localSeen = false;\n state = {\n ...remote,\n notifications,\n unseenCount: localSeen ? 0 : remote.unseenCount,\n unreadCount: countUnread(notifications),\n };\n emit();\n }\n\n function onTabMessage(event: { data: unknown }) {\n const message = event.data as TabMessage | null;\n if (!message || typeof message !== \"object\") return;\n if (role === \"leader\") {\n if (message.type === \"hello\") post({ type: \"state\", state });\n else if (message.type === \"mutated\") void refresh().catch(() => {});\n return;\n }\n if (message.type === \"state\") applyRemoteState(message.state);\n }\n\n // ---- leader / solo: polling with activity-shaped backoff ----\n\n function schedule(delay: number) {\n clearTimeout(timer);\n timer = setTimeout(tick, delay);\n }\n\n function nextDelay(): number {\n if (consecutiveFailures > 0) {\n return Math.min(pollIntervalMs * 2 ** consecutiveFailures, maxPollIntervalMs);\n }\n if (streamConnected) return safetyNetMs;\n if (now() < activeUntil) return pollIntervalMs;\n idleTicks += 1;\n return Math.min(pollIntervalMs * 2 ** idleTicks, maxPollIntervalMs);\n }\n\n async function tick() {\n if (!running) return;\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 if (running) schedule(nextDelay());\n }\n\n /** User input or a returning tab: poll fast again, and catch up now if the last refresh is stale. */\n function onActivity() {\n if (!running || role === \"follower\") return;\n activeUntil = now() + activeWindowMs;\n idleTicks = 0;\n if (!isHidden() && now() - lastRefreshAt > pollIntervalMs) {\n void refresh().catch(() => {});\n schedule(nextDelay());\n }\n }\n\n const onVisibility = () => {\n if (!isHidden()) onActivity();\n };\n\n const ACTIVITY_EVENTS = [\"focus\", \"pointerdown\", \"keydown\", \"online\"];\n\n // ---- leader / solo: the event stream ----\n\n function changed() {\n if (!running) return;\n void refresh().catch(() => {});\n }\n\n async function readStream(signal: AbortSignal): Promise<void> {\n const openedAt = now();\n const response = await doFetch(`${baseUrl}/events`, {\n headers: { accept: \"text/event-stream\" },\n signal,\n });\n if (!response.ok || !response.body) throw new Error(`GET /events failed: ${response.status}`);\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n let eventName = \"\";\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n let newline = buffer.indexOf(\"\\n\");\n while (newline >= 0) {\n const line = buffer.slice(0, newline).replace(/\\r$/, \"\");\n buffer = buffer.slice(newline + 1);\n if (line === \"\") {\n if (eventName === \"ready\") {\n streamConnected = true;\n streamFailures = 0;\n schedule(nextDelay());\n } else if (eventName === \"changed\") changed();\n eventName = \"\";\n } else if (line.startsWith(\"event:\")) eventName = line.slice(6).trim();\n newline = buffer.indexOf(\"\\n\");\n }\n }\n } finally {\n streamConnected = false;\n if (now() - openedAt < SHORT_STREAM_MS) shortStreams += 1;\n else shortStreams = 0;\n }\n }\n\n const isFollower = () => role === \"follower\";\n\n async function streamLoop() {\n while (running && !streamDisabled && !isFollower()) {\n streamAbort = new AbortController();\n try {\n await readStream(streamAbort.signal);\n } catch {\n if (!running) return;\n streamFailures += 1;\n }\n if (!running || isFollower()) return;\n if (shortStreams >= SHORT_STREAMS_BEFORE_GIVING_UP) {\n // The platform cuts streams too fast for them to be worth it here.\n streamDisabled = true;\n schedule(nextDelay());\n return;\n }\n await new Promise((resolve) =>\n setTimeout(resolve, Math.min(3_000 * 2 ** streamFailures, 60_000)),\n );\n }\n }\n\n // ---- roles ----\n\n function becomeLeader() {\n role = \"leader\";\n post({ type: \"leader\" });\n post({ type: \"state\", state });\n void tick();\n if (!streamDisabled) void streamLoop();\n }\n\n function becomeFollower() {\n role = \"follower\";\n post({ type: \"hello\" });\n }\n\n function acquireLeadership() {\n if (!locks || !channel) {\n role = \"solo\";\n void tick();\n if (!streamDisabled) void streamLoop();\n return;\n }\n becomeFollower();\n lockAbort = new AbortController();\n locks\n .request(\n `easy-ping:leader:${groupKey}`,\n { mode: \"exclusive\", signal: lockAbort.signal },\n () =>\n new Promise<void>((resolve) => {\n releaseLock = resolve;\n if (running) becomeLeader();\n else resolve();\n }),\n )\n .catch(() => {\n // aborted on stop, or Web Locks refused; either way no leadership here\n });\n }\n\n function start() {\n running = true;\n channel = makeChannel ? makeChannel() : undefined;\n channel?.addEventListener(\"message\", onTabMessage);\n serviceWorker?.addEventListener(\"message\", onRelay);\n if (activityTarget) {\n for (const name of ACTIVITY_EVENTS) activityTarget.addEventListener(name, onActivity);\n }\n if (typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", onVisibility);\n }\n activeUntil = now() + activeWindowMs;\n acquireLeadership();\n }\n\n function stop() {\n running = false;\n clearTimeout(timer);\n timer = undefined;\n streamAbort?.abort();\n streamAbort = undefined;\n lockAbort?.abort();\n lockAbort = undefined;\n releaseLock?.();\n releaseLock = undefined;\n channel?.removeEventListener(\"message\", onTabMessage);\n channel?.close();\n channel = undefined;\n serviceWorker?.removeEventListener(\"message\", onRelay);\n if (activityTarget) {\n for (const name of ACTIVITY_EVENTS) activityTarget.removeEventListener(name, onActivity);\n }\n if (typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", onVisibility);\n }\n role = \"solo\";\n }\n\n // ---- signals from outside: the service worker relay and the piggyback header ----\n\n function onRelay(event: { data?: unknown }) {\n const data = event.data as { source?: unknown; type?: unknown } | null;\n if (data?.source === \"easy-ping\" && data.type === \"changed\") changed();\n }\n\n let lastInboxVersion: string | null = null;\n\n // Only the app's own origin may steer the bell; a third-party response cannot.\n const sameOrigin = (response: Response) => {\n if (!response.url || typeof location === \"undefined\") return true;\n try {\n return new URL(response.url).origin === location.origin;\n } catch {\n return false;\n }\n };\n\n /**\n * Wraps your app's fetch so responses carrying the inbox-version header\n * refresh the bell when the version moves. Active users then never need a\n * dedicated poll. RFC 0006 idea 1.\n */\n function instrument(inner: typeof globalThis.fetch): typeof globalThis.fetch {\n return (async (...args: Parameters<typeof globalThis.fetch>) => {\n const response = await inner(...args);\n const version = sameOrigin(response) ? response.headers.get(\"x-easy-ping-inbox\") : null;\n if (version !== null && version !== lastInboxVersion) {\n const first = lastInboxVersion === null;\n lastInboxVersion = version;\n if (!first && role !== \"follower\") changed();\n }\n return response;\n }) as typeof globalThis.fetch;\n }\n\n function subscribe(listener: (state: NotifyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n\n // One transport regardless of how many components subscribe.\n if (listeners.size === 1) start();\n\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) stop();\n };\n }\n\n return {\n subscribe,\n refresh,\n loadMore,\n markAsRead,\n markAllRead,\n markSeen,\n instrument,\n getState: () => state,\n getTransport: (): TransportStatus => ({\n role,\n transport: streamDisabled ? \"poll\" : \"sse\",\n connected: streamConnected,\n }),\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 Transport = \"auto\" | \"sse\" | \"poll\";\n\nexport type TransportStatus = {\n /** \"leader\" holds this browser's single connection; \"follower\" mirrors it; \"solo\" has no Web Locks. */\n role: \"leader\" | \"follower\" | \"solo\";\n /** What the leader is using right now. */\n transport: \"sse\" | \"poll\";\n /** True while the event stream has said `ready` and not since closed. */\n connected: boolean;\n};\n\ntype LockManagerLike = {\n request(\n name: string,\n options: { mode: \"exclusive\"; signal?: AbortSignal },\n callback: () => Promise<void>,\n ): Promise<void>;\n};\n\ntype ChannelLike = {\n postMessage(message: unknown): void;\n addEventListener(type: \"message\", handler: (event: { data: unknown }) => void): void;\n removeEventListener(type: \"message\", handler: (event: { data: unknown }) => void): void;\n close(): void;\n};\n\ntype MessageTargetLike = {\n addEventListener(type: string, handler: (event: { data?: unknown }) => void): void;\n removeEventListener(type: string, handler: (event: { data?: unknown }) => void): void;\n};\n\nexport type NotifyClientOptions = {\n baseUrl?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n limit?: number | undefined;\n /** Poll interval while the user is active and no stream is connected. Default 15 s. */\n pollIntervalMs?: number | undefined;\n /** Ceiling the idle backoff grows to. Default 10 min. */\n maxPollIntervalMs?: number | undefined;\n /** Poll interval while the event stream is connected: a safety net. Default 5 min. */\n safetyNetMs?: number | undefined;\n /** How long after user input polling stays at the base interval. Default 60 s. */\n activeWindowMs?: number | undefined;\n /** \"auto\" streams when a DOM is present, \"poll\" never streams, \"sse\" always tries. */\n transport?: Transport | undefined;\n /** Injectable so the poller can be tested without a DOM. */\n isDocumentHidden?: (() => boolean) | undefined;\n /** Web Locks, for one connection per browser. Defaults to navigator.locks; null disables. */\n locks?: LockManagerLike | null | undefined;\n /** Cross-tab channel factory. Defaults to BroadcastChannel; null disables. */\n channel?: (() => ChannelLike) | null | undefined;\n /** Where push relays arrive. Defaults to navigator.serviceWorker; null disables. */\n serviceWorker?: MessageTargetLike | null | undefined;\n /** Where user activity is observed. Defaults to window; null disables. */\n activityTarget?: MessageTargetLike | null | undefined;\n now?: (() => number) | undefined;\n /**\n * Separates browsers' tab groups when identity is not a cookie: pass the\n * current user's id (or session id) so tabs signed in as different users\n * never share a leader or mirror each other's inbox.\n */\n scope?: string | 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\n/** A stream that dies this fast, repeatedly, is being cut by the platform: stop trying. */\nconst SHORT_STREAM_MS = 15_000;\nconst SHORT_STREAMS_BEFORE_GIVING_UP = 3;\n\ntype TabMessage =\n | { type: \"hello\" }\n | { type: \"state\"; state: NotifyState }\n | { type: \"mutated\" }\n | { type: \"leader\" };\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 ?? 10 * 60_000;\n const safetyNetMs = options.safetyNetMs ?? 5 * 60_000;\n const activeWindowMs = options.activeWindowMs ?? 60_000;\n const now = options.now ?? (() => Date.now());\n const groupKey = options.scope ? `${baseUrl}#${options.scope}` : baseUrl;\n const hasDom = typeof window !== \"undefined\" && typeof document !== \"undefined\";\n const transport: Transport = options.transport ?? \"auto\";\n const wantsStream = transport === \"sse\" || (transport === \"auto\" && hasDom);\n const isHidden =\n options.isDocumentHidden ??\n (() => typeof document !== \"undefined\" && document.visibilityState === \"hidden\");\n\n const locks =\n options.locks === undefined\n ? typeof navigator !== \"undefined\"\n ? ((navigator as { locks?: LockManagerLike }).locks ?? null)\n : null\n : options.locks;\n const makeChannel =\n options.channel === undefined\n ? typeof BroadcastChannel !== \"undefined\"\n ? () => new BroadcastChannel(`easy-ping:${groupKey}`) as unknown as ChannelLike\n : null\n : options.channel;\n const serviceWorker =\n options.serviceWorker === undefined\n ? typeof navigator !== \"undefined\"\n ? ((navigator as { serviceWorker?: MessageTargetLike }).serviceWorker ?? null)\n : null\n : options.serviceWorker;\n const activityTarget =\n options.activityTarget === undefined\n ? hasDom\n ? (window as unknown as MessageTargetLike)\n : null\n : options.activityTarget;\n\n let state: NotifyState = INITIAL;\n const listeners = new Set<(state: NotifyState) => void>();\n\n let role: TransportStatus[\"role\"] = \"solo\";\n let running = false;\n let channel: ChannelLike | undefined;\n let releaseLock: (() => void) | undefined;\n let lockAbort: AbortController | undefined;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let consecutiveFailures = 0;\n let idleTicks = 0;\n let activeUntil = 0;\n let lastRefreshAt = 0;\n let refreshing: Promise<void> | undefined;\n let refreshAgain = false;\n\n let streamAbort: AbortController | undefined;\n let streamConnected = false;\n let streamDisabled = !wantsStream;\n let shortStreams = 0;\n let streamFailures = 0;\n\n // Bumped per mutation so an in-flight poll cannot revert an optimistic update.\n let generation = 0;\n\n // A follower's own optimistic changes, kept until the leader's state reflects them.\n const localReads = new Set<string>();\n let localSeen = false;\n\n const stamp = () => new Date().toISOString();\n\n function emit() {\n for (const listener of listeners) listener(state);\n }\n\n function set(patch: Partial<NotifyState>) {\n state = { ...state, ...patch };\n state = { ...state, unreadCount: countUnread(state.notifications) };\n emit();\n if (role === \"leader\") post({ type: \"state\", state });\n }\n\n function post(message: TabMessage) {\n try {\n channel?.postMessage(message);\n } catch {\n // channel closed\n }\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 fetchPage(): Promise<void> {\n const startedAt = generation;\n\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 /** Coalesces overlapping calls: a second request while one is in flight runs once after it. */\n function refresh(): Promise<void> {\n if (refreshing) {\n refreshAgain = true;\n return refreshing;\n }\n refreshing = (async () => {\n try {\n do {\n refreshAgain = false;\n await fetchPage();\n lastRefreshAt = now();\n } while (refreshAgain);\n } finally {\n refreshing = undefined;\n }\n })();\n return refreshing;\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 if (role === \"follower\") post({ type: \"mutated\" });\n } catch (error) {\n generation += 1;\n state = previous;\n emit();\n throw error;\n }\n }\n\n async function markAsRead(id: string): Promise<void> {\n if (role === \"follower\") localReads.add(id);\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 ).catch((error) => {\n localReads.delete(id);\n throw error;\n });\n }\n\n async function markAllRead(): Promise<void> {\n if (role === \"follower\") for (const row of state.notifications) localReads.add(row.id);\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, readAt: row.readAt ?? stamp() })),\n {},\n () => request(\"/read-all\", { method: \"POST\" }),\n ).catch((error) => {\n localReads.clear();\n throw error;\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 if (role === \"follower\") localSeen = true;\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 ).catch((error) => {\n localSeen = false;\n throw error;\n });\n }\n\n // ---- follower: mirror the leader, keep own optimistic changes until confirmed ----\n\n function applyRemoteState(remote: NotifyState) {\n const notifications = remote.notifications.map((row) => {\n if (!localReads.has(row.id)) return row;\n if (row.readAt) {\n localReads.delete(row.id);\n return row;\n }\n return { ...row, readAt: stamp() };\n });\n if (localSeen && remote.unseenCount === 0) localSeen = false;\n state = {\n ...remote,\n notifications,\n unseenCount: localSeen ? 0 : remote.unseenCount,\n unreadCount: countUnread(notifications),\n };\n emit();\n }\n\n function onTabMessage(event: { data: unknown }) {\n const message = event.data as TabMessage | null;\n if (!message || typeof message !== \"object\") return;\n if (role === \"leader\") {\n if (message.type === \"hello\") post({ type: \"state\", state });\n else if (message.type === \"mutated\") void refresh().catch(() => {});\n return;\n }\n if (message.type === \"state\") applyRemoteState(message.state);\n }\n\n // ---- leader / solo: polling with activity-shaped backoff ----\n\n function schedule(delay: number) {\n clearTimeout(timer);\n timer = setTimeout(tick, delay);\n }\n\n function nextDelay(): number {\n if (consecutiveFailures > 0) {\n return Math.min(pollIntervalMs * 2 ** consecutiveFailures, maxPollIntervalMs);\n }\n if (streamConnected) return safetyNetMs;\n if (now() < activeUntil) return pollIntervalMs;\n idleTicks += 1;\n return Math.min(pollIntervalMs * 2 ** idleTicks, maxPollIntervalMs);\n }\n\n async function tick() {\n if (!running) return;\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 if (running) schedule(nextDelay());\n }\n\n /** User input or a returning tab: poll fast again, and catch up now if the last refresh is stale. */\n function onActivity() {\n if (!running || role === \"follower\") return;\n activeUntil = now() + activeWindowMs;\n idleTicks = 0;\n if (!isHidden() && now() - lastRefreshAt > pollIntervalMs) {\n void refresh().catch(() => {});\n schedule(nextDelay());\n }\n }\n\n const onVisibility = () => {\n if (!isHidden()) onActivity();\n };\n\n const ACTIVITY_EVENTS = [\"focus\", \"pointerdown\", \"keydown\", \"online\"];\n\n // ---- leader / solo: the event stream ----\n\n function changed() {\n if (!running) return;\n void refresh().catch(() => {});\n }\n\n async function readStream(signal: AbortSignal): Promise<void> {\n const openedAt = now();\n const response = await doFetch(`${baseUrl}/events`, {\n headers: { accept: \"text/event-stream\" },\n signal,\n });\n if (!response.ok) throw new Error(`GET /events failed: ${response.status}`);\n if (!response.body) {\n // This fetch cannot stream a body (React Native's built-in one, some polyfills): poll instead.\n streamDisabled = true;\n schedule(nextDelay());\n return;\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n let eventName = \"\";\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n let newline = buffer.indexOf(\"\\n\");\n while (newline >= 0) {\n const line = buffer.slice(0, newline).replace(/\\r$/, \"\");\n buffer = buffer.slice(newline + 1);\n if (line === \"\") {\n if (eventName === \"ready\") {\n streamConnected = true;\n streamFailures = 0;\n schedule(nextDelay());\n } else if (eventName === \"changed\") changed();\n eventName = \"\";\n } else if (line.startsWith(\"event:\")) eventName = line.slice(6).trim();\n newline = buffer.indexOf(\"\\n\");\n }\n }\n } finally {\n streamConnected = false;\n if (now() - openedAt < SHORT_STREAM_MS) shortStreams += 1;\n else shortStreams = 0;\n }\n }\n\n const isFollower = () => role === \"follower\";\n\n async function streamLoop() {\n while (running && !streamDisabled && !isFollower()) {\n streamAbort = new AbortController();\n try {\n await readStream(streamAbort.signal);\n } catch {\n if (!running) return;\n streamFailures += 1;\n }\n if (!running || isFollower() || streamDisabled) return;\n if (shortStreams >= SHORT_STREAMS_BEFORE_GIVING_UP) {\n // The platform cuts streams too fast for them to be worth it here.\n streamDisabled = true;\n schedule(nextDelay());\n return;\n }\n await new Promise((resolve) =>\n setTimeout(resolve, Math.min(3_000 * 2 ** streamFailures, 60_000)),\n );\n }\n }\n\n // ---- roles ----\n\n function becomeLeader() {\n role = \"leader\";\n post({ type: \"leader\" });\n post({ type: \"state\", state });\n void tick();\n if (!streamDisabled) void streamLoop();\n }\n\n function becomeFollower() {\n role = \"follower\";\n post({ type: \"hello\" });\n }\n\n function acquireLeadership() {\n if (!locks || !channel) {\n role = \"solo\";\n void tick();\n if (!streamDisabled) void streamLoop();\n return;\n }\n becomeFollower();\n lockAbort = new AbortController();\n locks\n .request(\n `easy-ping:leader:${groupKey}`,\n { mode: \"exclusive\", signal: lockAbort.signal },\n () =>\n new Promise<void>((resolve) => {\n releaseLock = resolve;\n if (running) becomeLeader();\n else resolve();\n }),\n )\n .catch(() => {\n // aborted on stop, or Web Locks refused; either way no leadership here\n });\n }\n\n function start() {\n running = true;\n channel = makeChannel ? makeChannel() : undefined;\n channel?.addEventListener(\"message\", onTabMessage);\n serviceWorker?.addEventListener(\"message\", onRelay);\n if (activityTarget) {\n for (const name of ACTIVITY_EVENTS) activityTarget.addEventListener(name, onActivity);\n }\n if (typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", onVisibility);\n }\n activeUntil = now() + activeWindowMs;\n acquireLeadership();\n }\n\n function stop() {\n running = false;\n clearTimeout(timer);\n timer = undefined;\n streamAbort?.abort();\n streamAbort = undefined;\n lockAbort?.abort();\n lockAbort = undefined;\n releaseLock?.();\n releaseLock = undefined;\n channel?.removeEventListener(\"message\", onTabMessage);\n channel?.close();\n channel = undefined;\n serviceWorker?.removeEventListener(\"message\", onRelay);\n if (activityTarget) {\n for (const name of ACTIVITY_EVENTS) activityTarget.removeEventListener(name, onActivity);\n }\n if (typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", onVisibility);\n }\n role = \"solo\";\n }\n\n // ---- signals from outside: the service worker relay and the piggyback header ----\n\n function onRelay(event: { data?: unknown }) {\n const data = event.data as { source?: unknown; type?: unknown } | null;\n if (data?.source === \"easy-ping\" && data.type === \"changed\") changed();\n }\n\n let lastInboxVersion: string | null = null;\n\n // Only the app's own origin may steer the bell; a third-party response cannot.\n const sameOrigin = (response: Response) => {\n if (!response.url || typeof location === \"undefined\") return true;\n try {\n return new URL(response.url).origin === location.origin;\n } catch {\n return false;\n }\n };\n\n /**\n * Wraps your app's fetch so responses carrying the inbox-version header\n * refresh the bell when the version moves. Active users then never need a\n * dedicated poll. RFC 0006 idea 1.\n */\n function instrument(inner: typeof globalThis.fetch): typeof globalThis.fetch {\n return (async (...args: Parameters<typeof globalThis.fetch>) => {\n const response = await inner(...args);\n const version = sameOrigin(response) ? response.headers.get(\"x-easy-ping-inbox\") : null;\n if (version !== null && version !== lastInboxVersion) {\n const first = lastInboxVersion === null;\n lastInboxVersion = version;\n if (!first && role !== \"follower\") changed();\n }\n return response;\n }) as typeof globalThis.fetch;\n }\n\n function subscribe(listener: (state: NotifyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n\n // One transport regardless of how many components subscribe.\n if (listeners.size === 1) start();\n\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) stop();\n };\n }\n\n return {\n subscribe,\n refresh,\n loadMore,\n markAsRead,\n markAllRead,\n markSeen,\n instrument,\n getState: () => state,\n getTransport: (): TransportStatus => ({\n role,\n transport: streamDisabled ? \"poll\" : \"sse\",\n connected: streamConnected,\n }),\n };\n}\n\nexport type NotifyClient = ReturnType<typeof createNotifyClient>;\n"]}
|
package/dist/client.js
CHANGED
|
@@ -261,7 +261,12 @@ function createNotifyClient(options = {}) {
|
|
|
261
261
|
headers: { accept: "text/event-stream" },
|
|
262
262
|
signal
|
|
263
263
|
});
|
|
264
|
-
if (!response.ok
|
|
264
|
+
if (!response.ok) throw new Error(`GET /events failed: ${response.status}`);
|
|
265
|
+
if (!response.body) {
|
|
266
|
+
streamDisabled = true;
|
|
267
|
+
schedule(nextDelay());
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
265
270
|
const reader = response.body.getReader();
|
|
266
271
|
const decoder = new TextDecoder();
|
|
267
272
|
let buffer = "";
|
|
@@ -302,7 +307,7 @@ function createNotifyClient(options = {}) {
|
|
|
302
307
|
if (!running) return;
|
|
303
308
|
streamFailures += 1;
|
|
304
309
|
}
|
|
305
|
-
if (!running || isFollower()) return;
|
|
310
|
+
if (!running || isFollower() || streamDisabled) return;
|
|
306
311
|
if (shortStreams >= SHORT_STREAMS_BEFORE_GIVING_UP) {
|
|
307
312
|
streamDisabled = true;
|
|
308
313
|
schedule(nextDelay());
|