easy-ping 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{chunk-USQ6CQTJ.js → chunk-JOVIZXO6.js} +105 -54
- package/dist/chunk-JOVIZXO6.js.map +1 -0
- package/dist/client.cjs +12 -3
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +6 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +12 -3
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +106 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/node.cjs.map +1 -1
- package/dist/node.js +1 -1
- package/dist/plugins/preferences.cjs +2 -2
- package/dist/plugins/preferences.cjs.map +1 -1
- package/dist/plugins/preferences.js +1 -1
- package/dist/plugins/push.cjs +2 -2
- package/dist/plugins/push.cjs.map +1 -1
- package/dist/plugins/push.js +1 -1
- package/dist/react.cjs +12 -3
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +6 -0
- package/dist/react.d.ts +6 -0
- package/dist/react.js +12 -3
- package/dist/react.js.map +1 -1
- package/package.json +4 -3
- package/dist/chunk-USQ6CQTJ.js.map +0 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ database, with no per-notification bill.
|
|
|
11
11
|
[](https://www.npmjs.com/package/easy-ping)
|
|
12
12
|
[](./LICENSE)
|
|
13
13
|
|
|
14
|
-
[Documentation](https://easy-
|
|
14
|
+
[Documentation](https://easy-ping.teklumoges.dev) · [Quickstart](https://easy-ping.teklumoges.dev/docs/quickstart) · [Changelog](https://easy-ping.teklumoges.dev/docs/changelog) · [AI context](https://easy-ping.teklumoges.dev/docs/ai-assistant)
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
@@ -81,15 +81,16 @@ function createScopedSigner(secret, pluginId, purposes, onViolation) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// src/core/handler.ts
|
|
84
|
+
var STREAM_HIGH_WATER_MARK = 256;
|
|
84
85
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
85
86
|
var MAX_READ_IDS = 200;
|
|
86
87
|
var PRIVATE_HEADERS = {
|
|
87
88
|
"cache-control": "private, no-store",
|
|
88
89
|
"x-content-type-options": "nosniff"
|
|
89
90
|
};
|
|
90
|
-
var json = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
91
|
+
var json = (body, status = 200, headers = {}) => new Response(JSON.stringify(body), {
|
|
91
92
|
status,
|
|
92
|
-
headers: { "content-type": "application/json", ...PRIVATE_HEADERS }
|
|
93
|
+
headers: { "content-type": "application/json", ...PRIVATE_HEADERS, ...headers }
|
|
93
94
|
});
|
|
94
95
|
var empty = (status) => new Response(null, { status, headers: PRIVATE_HEADERS });
|
|
95
96
|
function harden(response) {
|
|
@@ -255,7 +256,14 @@ function createHandler(deps) {
|
|
|
255
256
|
if (userId === "error") return empty(500);
|
|
256
257
|
if (userId === null) return empty(401);
|
|
257
258
|
if (request.method === "GET" && path === "/events") {
|
|
258
|
-
|
|
259
|
+
if (!deps.events) return empty(404);
|
|
260
|
+
if ((openStreams.get(userId) ?? 0) >= deps.events.maxStreamsPerUser) {
|
|
261
|
+
return json({ error: "too many open event streams" }, 429, { "retry-after": "30" });
|
|
262
|
+
}
|
|
263
|
+
if (totalStreams >= deps.events.maxStreams) {
|
|
264
|
+
return json({ error: "event streams unavailable" }, 503, { "retry-after": "30" });
|
|
265
|
+
}
|
|
266
|
+
return events(request, userId, deps.events);
|
|
259
267
|
}
|
|
260
268
|
if (request.method === "GET" && path === "/") {
|
|
261
269
|
const limitParam = Number(url.searchParams.get("limit") ?? 20);
|
|
@@ -303,6 +311,18 @@ function createHandler(deps) {
|
|
|
303
311
|
}
|
|
304
312
|
return empty(404);
|
|
305
313
|
}
|
|
314
|
+
const openStreams = /* @__PURE__ */ new Map();
|
|
315
|
+
let totalStreams = 0;
|
|
316
|
+
const trackStream = (userId) => {
|
|
317
|
+
openStreams.set(userId, (openStreams.get(userId) ?? 0) + 1);
|
|
318
|
+
totalStreams += 1;
|
|
319
|
+
return () => {
|
|
320
|
+
const remaining = (openStreams.get(userId) ?? 1) - 1;
|
|
321
|
+
if (remaining > 0) openStreams.set(userId, remaining);
|
|
322
|
+
else openStreams.delete(userId);
|
|
323
|
+
totalStreams -= 1;
|
|
324
|
+
};
|
|
325
|
+
};
|
|
306
326
|
async function fingerprint(userId) {
|
|
307
327
|
const [page, unseen] = await Promise.all([
|
|
308
328
|
deps.adapter.listNotifications({ userId, limit: 1 }),
|
|
@@ -311,63 +331,92 @@ function createHandler(deps) {
|
|
|
311
331
|
const newest = page.notifications[0];
|
|
312
332
|
return `${newest?.id ?? ""}|${newest?.readAt ? 1 : 0}|${unseen}`;
|
|
313
333
|
}
|
|
334
|
+
const probes = /* @__PURE__ */ new Map();
|
|
335
|
+
function subscribeProbe(userId, onChanged, intervalMs) {
|
|
336
|
+
let probe = probes.get(userId);
|
|
337
|
+
if (!probe) {
|
|
338
|
+
const created = {
|
|
339
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
340
|
+
timer: setInterval(() => {
|
|
341
|
+
fingerprint(userId).then((value) => {
|
|
342
|
+
if (created.last !== void 0 && value !== created.last) {
|
|
343
|
+
for (const listener of created.listeners) listener();
|
|
344
|
+
}
|
|
345
|
+
created.last = value;
|
|
346
|
+
}).catch((error) => deps.logger.error("events probe failed", { error }));
|
|
347
|
+
}, intervalMs)
|
|
348
|
+
};
|
|
349
|
+
probe = created;
|
|
350
|
+
probes.set(userId, created);
|
|
351
|
+
fingerprint(userId).then((value) => {
|
|
352
|
+
if (created.last === void 0) created.last = value;
|
|
353
|
+
}).catch(() => {
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
probe.listeners.add(onChanged);
|
|
357
|
+
return () => {
|
|
358
|
+
const current = probes.get(userId);
|
|
359
|
+
if (!current) return;
|
|
360
|
+
current.listeners.delete(onChanged);
|
|
361
|
+
if (current.listeners.size === 0) {
|
|
362
|
+
clearInterval(current.timer);
|
|
363
|
+
probes.delete(userId);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
314
367
|
function events(request, userId, settings) {
|
|
315
368
|
const encoder3 = new TextEncoder();
|
|
316
369
|
const timers = [];
|
|
317
|
-
|
|
370
|
+
const unsubscribes = [];
|
|
318
371
|
let closed = false;
|
|
319
372
|
let cleanup = () => {
|
|
320
373
|
};
|
|
321
|
-
const stream = new ReadableStream(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
controller.
|
|
327
|
-
|
|
328
|
-
|
|
374
|
+
const stream = new ReadableStream(
|
|
375
|
+
{
|
|
376
|
+
start(controller) {
|
|
377
|
+
const write = (text) => {
|
|
378
|
+
if (closed) return;
|
|
379
|
+
if (controller.desiredSize !== null && controller.desiredSize <= 0) {
|
|
380
|
+
cleanup();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
controller.enqueue(encoder3.encode(text));
|
|
385
|
+
} catch {
|
|
386
|
+
cleanup();
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
cleanup = () => {
|
|
390
|
+
if (closed) return;
|
|
391
|
+
closed = true;
|
|
392
|
+
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
393
|
+
for (const timer of timers) clearInterval(timer);
|
|
394
|
+
try {
|
|
395
|
+
controller.close();
|
|
396
|
+
} catch {
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
const changed = () => write("event: changed\ndata: {}\n\n");
|
|
400
|
+
unsubscribes.push(trackStream(userId));
|
|
401
|
+
write("retry: 3000\n\nevent: ready\ndata: {}\n\n");
|
|
402
|
+
unsubscribes.push(deps.signals.subscribe(inboxChannel(userId), changed));
|
|
403
|
+
timers.push(setInterval(() => write(": keepalive\n\n"), settings.heartbeatMs));
|
|
404
|
+
if (!deps.signals.crossProcess && settings.probeIntervalMs > 0) {
|
|
405
|
+
unsubscribes.push(subscribeProbe(userId, changed, settings.probeIntervalMs));
|
|
329
406
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
unsubscribe?.();
|
|
335
|
-
for (const timer of timers) clearInterval(timer);
|
|
336
|
-
try {
|
|
337
|
-
controller.close();
|
|
338
|
-
} catch {
|
|
407
|
+
if (settings.maxDurationMs > 0) {
|
|
408
|
+
timers.push(
|
|
409
|
+
setTimeout(cleanup, settings.maxDurationMs)
|
|
410
|
+
);
|
|
339
411
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
timers.push(setInterval(() => write(": keepalive\n\n"), settings.heartbeatMs));
|
|
345
|
-
if (!deps.signals.crossProcess && settings.probeIntervalMs > 0) {
|
|
346
|
-
let last;
|
|
347
|
-
fingerprint(userId).then((value) => {
|
|
348
|
-
last = value;
|
|
349
|
-
}).catch(() => {
|
|
350
|
-
});
|
|
351
|
-
timers.push(
|
|
352
|
-
setInterval(() => {
|
|
353
|
-
fingerprint(userId).then((value) => {
|
|
354
|
-
if (last !== void 0 && value !== last) changed();
|
|
355
|
-
last = value;
|
|
356
|
-
}).catch((error) => deps.logger.error("events probe failed", { error }));
|
|
357
|
-
}, settings.probeIntervalMs)
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
if (settings.maxDurationMs > 0) {
|
|
361
|
-
timers.push(
|
|
362
|
-
setTimeout(cleanup, settings.maxDurationMs)
|
|
363
|
-
);
|
|
412
|
+
request.signal?.addEventListener("abort", cleanup, { once: true });
|
|
413
|
+
},
|
|
414
|
+
cancel() {
|
|
415
|
+
cleanup();
|
|
364
416
|
}
|
|
365
|
-
request.signal?.addEventListener("abort", cleanup, { once: true });
|
|
366
417
|
},
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
});
|
|
418
|
+
{ highWaterMark: STREAM_HIGH_WATER_MARK }
|
|
419
|
+
);
|
|
371
420
|
return new Response(stream, {
|
|
372
421
|
headers: {
|
|
373
422
|
"content-type": "text/event-stream; charset=utf-8",
|
|
@@ -379,13 +428,15 @@ function createHandler(deps) {
|
|
|
379
428
|
});
|
|
380
429
|
}
|
|
381
430
|
async function handle(request) {
|
|
431
|
+
let response;
|
|
382
432
|
try {
|
|
383
|
-
|
|
433
|
+
response = await route(request);
|
|
434
|
+
return response;
|
|
384
435
|
} catch (error) {
|
|
385
436
|
deps.logger.error("request failed", { url: request.url, error });
|
|
386
437
|
return empty(500);
|
|
387
438
|
} finally {
|
|
388
|
-
deps.sweepOnRequest?.();
|
|
439
|
+
if (response && response.status < 400) deps.sweepOnRequest?.();
|
|
389
440
|
}
|
|
390
441
|
}
|
|
391
442
|
return {
|
|
@@ -396,5 +447,5 @@ function createHandler(deps) {
|
|
|
396
447
|
}
|
|
397
448
|
|
|
398
449
|
export { DEFAULT_TOKEN_TTL_SECONDS, MAX_BODY_BYTES, MAX_TOKEN_TTL_SECONDS, createHandler, createScopedSigner, expiresIn, readJsonBody, signToken, verifyToken };
|
|
399
|
-
//# sourceMappingURL=chunk-
|
|
400
|
-
//# sourceMappingURL=chunk-
|
|
450
|
+
//# sourceMappingURL=chunk-JOVIZXO6.js.map
|
|
451
|
+
//# sourceMappingURL=chunk-JOVIZXO6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/tokens.ts","../src/core/handler.ts"],"names":["encoder","route"],"mappings":"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","file":"chunk-JOVIZXO6.js","sourcesContent":["import { decodeBase64Url, decodeBase64UrlBytes, encodeBase64Url } from \"./base64url\";\n\nexport type TokenClaims = {\n /** Subject — the user the token acts for. */\n uid: string;\n /** What the token is allowed to do. Verification requires an exact match. */\n purpose: string;\n /** Expiry, epoch seconds. */\n exp: number;\n /** Issued at, epoch seconds. Set by signToken; lets a route refuse a token older than a later change. */\n iat?: number;\n /** Free-form, purpose-specific. */\n data?: Record<string, string>;\n};\n\nconst encoder = new TextEncoder();\n\n// Changing this invalidates every outstanding token. Bump it on purpose only.\nconst KEY_SALT = encoder.encode(\"easy-ping/tokens/v1\");\n\n/** One HMAC key per purpose, derived with HKDF, so an unsubscribe key signs nothing else. */\nasync function hmacKey(secret: string, purpose: string): Promise<CryptoKey> {\n const master = await crypto.subtle.importKey(\"raw\", encoder.encode(secret), \"HKDF\", false, [\n \"deriveKey\",\n ]);\n return crypto.subtle.deriveKey(\n { name: \"HKDF\", hash: \"SHA-256\", salt: KEY_SALT, info: encoder.encode(purpose) },\n master,\n { name: \"HMAC\", hash: \"SHA-256\", length: 256 },\n false,\n [\"sign\", \"verify\"],\n );\n}\n\nexport async function signToken(\n secret: string,\n claims: TokenClaims,\n now: Date = new Date(),\n): Promise<string> {\n const stamped: TokenClaims = { ...claims, iat: claims.iat ?? Math.floor(now.getTime() / 1000) };\n const body = encodeBase64Url(JSON.stringify(stamped));\n const signature = await crypto.subtle.sign(\n \"HMAC\",\n await hmacKey(secret, claims.purpose),\n encoder.encode(body),\n );\n return `${body}.${encodeBase64Url(new Uint8Array(signature))}`;\n}\n\n/** null for every failure mode. Distinguishing them tells an attacker which part to fix. */\nexport async function verifyToken(\n secret: string,\n token: string,\n purpose: string,\n now: Date = new Date(),\n): Promise<TokenClaims | null> {\n const separator = token.lastIndexOf(\".\");\n if (separator <= 0) return null;\n\n const body = token.slice(0, separator);\n const signature = decodeBase64UrlBytes(token.slice(separator + 1));\n if (!signature) return null;\n\n // Verified under the expected purpose's key before the body is parsed. subtle.verify is constant-time.\n const valid = await crypto.subtle.verify(\n \"HMAC\",\n await hmacKey(secret, purpose),\n signature,\n encoder.encode(body),\n );\n if (!valid) return null;\n\n const decoded = decodeBase64Url(body);\n if (!decoded) return null;\n\n let claims: TokenClaims;\n try {\n claims = JSON.parse(decoded) as TokenClaims;\n } catch {\n return null;\n }\n\n if (typeof claims?.uid !== \"string\" || typeof claims.exp !== \"number\") return null;\n if (claims.purpose !== purpose) return null;\n if (claims.exp * 1000 <= now.getTime()) return null;\n if (claims.iat !== undefined && typeof claims.iat !== \"number\") return null;\n\n if (claims.data !== undefined) {\n if (typeof claims.data !== \"object\" || claims.data === null || Array.isArray(claims.data)) {\n return null;\n }\n for (const value of Object.values(claims.data)) {\n if (typeof value !== \"string\") return null;\n }\n }\n\n return claims;\n}\n\nexport const DEFAULT_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;\n/** Ninety days. A link that must outlive this should be re-issued, not made immortal. */\nexport const MAX_TOKEN_TTL_SECONDS = 90 * 24 * 60 * 60;\n\nexport const expiresIn = (seconds: number, now: Date = new Date()): number =>\n Math.floor(now.getTime() / 1000) + Math.min(Math.max(seconds, 0), MAX_TOKEN_TTL_SECONDS);\n\nexport type SignRequest = {\n uid: string;\n purpose: string;\n data?: Record<string, string>;\n /** Defaults to 30 days, capped at 90. */\n ttlSeconds?: number;\n};\n\n/** What a plugin gets instead of the secret: signs only its own declared purposes. */\nexport function createScopedSigner(\n secret: string,\n pluginId: string,\n purposes: ReadonlySet<string>,\n onViolation: (message: string) => Error,\n): (request: SignRequest) => Promise<string> {\n return (request) => {\n if (!purposes.has(request.purpose)) {\n throw onViolation(\n `plugin \"${pluginId}\" tried to sign a token for purpose \"${request.purpose}\", ` +\n \"which none of its signed routes declare.\",\n );\n }\n return signToken(secret, {\n uid: request.uid,\n purpose: request.purpose,\n exp: expiresIn(request.ttlSeconds ?? DEFAULT_TOKEN_TTL_SECONDS),\n ...(request.data ? { data: request.data } : {}),\n });\n };\n}\n","import type { DatabaseAdapter } from \"./adapter\";\nimport type { SessionConfig } from \"./config\";\nimport type { Logger } from \"./errors\";\nimport type { Promisable, RouteDefinition } from \"./plugin\";\nimport type { Runner } from \"./runner\";\nimport { inboxChannel, type Signals } from \"./signals\";\nimport { verifyToken } from \"./tokens\";\n\nexport type EventsSettings = {\n heartbeatMs: number;\n probeIntervalMs: number;\n maxDurationMs: number;\n maxStreamsPerUser: number;\n maxStreams: number;\n};\n\n/** Unread chunks a stream may hold before it is closed on a consumer that stopped reading. */\nconst STREAM_HIGH_WATER_MARK = 256;\n\nexport type HandlerDeps = {\n adapter: DatabaseAdapter;\n session: SessionConfig;\n runner: Runner;\n logger: Logger;\n basePath: string;\n secret: string;\n signals: Signals;\n /** `false` unmounts GET /events. */\n events: EventsSettings | false;\n /** Fire-and-forget delivery pass after a request. Rate-limited by the instance. */\n sweepOnRequest?: (() => void) | undefined;\n cronSecret?: string | undefined;\n /** Guards plugin machine routes. The instance defaults it to cronSecret. */\n machineSecret?: string | undefined;\n cronMaxSweeps?: number | undefined;\n pluginRoutes?: readonly RouteDefinition[] | undefined;\n /** Returns a 429 to send, or null to proceed. Runs after onRequest, before routing. */\n rateLimit?: ((request: Request) => Response | null) | undefined;\n /** Origins other than the request's own host that may POST. `*.example.com` allowed. */\n trustedOrigins?: readonly string[] | undefined;\n /** Runs before routing. Returning a Response short-circuits; use it for rate limiting. */\n // biome-ignore lint/suspicious/noConfusingVoidType: a hook that returns nothing is the common case\n onRequest?: ((request: Request) => Promisable<Response | undefined | void>) | undefined;\n maxBodyBytes?: number | undefined;\n};\n\n/** Every JSON body is capped here. Nothing legitimate the routes accept is bigger. */\nexport const MAX_BODY_BYTES = 64 * 1024;\n\nconst MAX_READ_IDS = 200;\n\n// Per-user data: no shared caching, no sniffing.\nconst PRIVATE_HEADERS: Record<string, string> = {\n \"cache-control\": \"private, no-store\",\n \"x-content-type-options\": \"nosniff\",\n};\n\nconst json = (body: unknown, status = 200, headers: Record<string, string> = {}) =>\n new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\", ...PRIVATE_HEADERS, ...headers },\n });\n\nconst empty = (status: number) => new Response(null, { status, headers: PRIVATE_HEADERS });\n\n/** Plugin responses get the same headers, without touching their body. */\nfunction harden(response: Response): Response {\n const headers = new Headers(response.headers);\n for (const [name, value] of Object.entries(PRIVATE_HEADERS)) {\n if (!headers.has(name)) headers.set(name, value);\n }\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n\nconst encoder = new TextEncoder();\n\n/** Constant time over SHA-256 digests, so neither content nor length leaks. Works on edge runtimes. */\nasync function timingSafeEqual(a: string, b: string): Promise<boolean> {\n const [left, right] = await Promise.all(\n [a, b].map(\n async (value) => new Uint8Array(await crypto.subtle.digest(\"SHA-256\", encoder.encode(value))),\n ),\n );\n let diff = 0;\n for (let i = 0; i < 32; i += 1) diff |= (left?.[i] ?? 0) ^ (right?.[i] ?? 0);\n return diff === 0;\n}\n\n/** Anchored at a segment boundary: `/foo/api/notifications/cron` is not a cron request. */\nfunction routePath(url: URL, basePath: string): string {\n const { pathname } = url;\n if (pathname === basePath) return \"/\";\n if (pathname.startsWith(`${basePath}/`)) return pathname.slice(basePath.length);\n return pathname;\n}\n\nexport type JsonBody = { body: Record<string, unknown> } | { error: Response };\n\n/** Reads a JSON object body with a streaming byte cap. Malformed JSON is 400; an empty body is `{}`. */\nexport async function readJsonBody(\n request: Request,\n maxBytes: number = MAX_BODY_BYTES,\n): Promise<JsonBody> {\n const tooLarge = () => ({ error: json({ error: `body exceeds ${maxBytes} bytes` }, 413) });\n\n const declared = Number(request.headers.get(\"content-length\"));\n if (Number.isFinite(declared) && declared > maxBytes) return tooLarge();\n if (!request.body) return { body: {} };\n\n const reader = request.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n return tooLarge();\n }\n chunks.push(value);\n }\n\n const bytes = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n bytes.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n const text = new TextDecoder().decode(bytes);\n if (text.trim() === \"\") return { body: {} };\n\n try {\n const parsed: unknown = JSON.parse(text);\n return {\n body:\n parsed && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {},\n };\n } catch {\n return { error: json({ error: \"body must be a JSON object\" }, 400) };\n }\n}\n\nconst isJsonContentType = (request: Request) =>\n /^application\\/json\\s*(;|$)/i.test((request.headers.get(\"content-type\") ?? \"\").trim());\n\nfunction originAllowed(origin: string, url: URL, request: Request, trusted: readonly string[]) {\n let host: string;\n try {\n host = new URL(origin).host;\n } catch {\n return false;\n }\n\n if (host === url.host) return true;\n // Behind a proxy that rewrites Host, the public host arrives here instead.\n const forwarded = request.headers.get(\"x-forwarded-host\")?.split(\",\")[0]?.trim();\n if (forwarded && host === forwarded) return true;\n\n return trusted.some((entry) =>\n entry.startsWith(\"*.\") ? host.endsWith(entry.slice(1)) : entry === origin,\n );\n}\n\n/** CSRF: a cross-site form can neither send `application/json` without a preflight nor forge `Origin`. */\nfunction csrfCheck(request: Request, url: URL, trusted: readonly string[]): Response | null {\n if (!isJsonContentType(request)) {\n return json({ error: \"content-type must be application/json\" }, 415);\n }\n\n const origin = request.headers.get(\"origin\");\n if (origin === null) return null;\n if (origin === \"null\" || !originAllowed(origin, url, request, trusted)) return empty(403);\n\n return null;\n}\n\nexport function createHandler(deps: HandlerDeps) {\n const trustedOrigins = deps.trustedOrigins ?? [];\n const maxBodyBytes = deps.maxBodyBytes ?? MAX_BODY_BYTES;\n\n async function resolveUserId(request: Request): Promise<string | null | \"error\"> {\n try {\n return await deps.session.getUserId(request);\n } catch (error) {\n // A broken session lookup is a 500, never a 401. RFC 0002 §1.\n deps.logger.error(\"session.getUserId threw\", { error });\n return \"error\";\n }\n }\n\n async function bearerMatches(request: Request, secret: string | undefined): Promise<boolean> {\n if (!secret) return false;\n const header = request.headers.get(\"authorization\") ?? \"\";\n const presented = header.startsWith(\"Bearer \") ? header.slice(7) : header;\n return timingSafeEqual(presented, secret);\n }\n\n async function handleCron(request: Request): Promise<Response> {\n if (!deps.cronSecret) return empty(404);\n if (!(await bearerMatches(request, deps.cronSecret))) return empty(401);\n\n // Bounded so an overlapping scheduler does not hold a request open for the whole backlog.\n const result = await deps.runner.drain(\n deps.cronMaxSweeps === undefined ? {} : { maxSweeps: deps.cronMaxSweeps },\n );\n return json(result);\n }\n\n /** Plugin routes first; auth is enforced here so a plugin cannot forget it. */\n async function handlePluginRoute(\n route: RouteDefinition,\n request: Request,\n url: URL,\n ): Promise<Response> {\n if (route.scope.type === \"user\") {\n if (request.method === \"POST\") {\n const rejected = csrfCheck(request, url, trustedOrigins);\n if (rejected) return rejected;\n }\n\n const userId = await resolveUserId(request);\n if (userId === \"error\") return empty(500);\n if (userId === null) return empty(401);\n return harden(await route.handler({ request, userId, claims: null, params: {} }));\n }\n\n if (route.scope.type === \"machine\") {\n if (!deps.machineSecret) return empty(404);\n if (!(await bearerMatches(request, deps.machineSecret))) return empty(401);\n return harden(await route.handler({ request, userId: null, claims: null, params: {} }));\n }\n\n if (route.scope.type === \"signed\") {\n const token =\n url.searchParams.get(\"token\") ??\n (request.headers.get(\"authorization\") ?? \"\").replace(/^Bearer /, \"\");\n\n const claims = await verifyToken(deps.secret, token, route.scope.purpose);\n // One response for every failure mode. RFC 0002 §5.\n if (!claims) return empty(400);\n\n return harden(await route.handler({ request, userId: claims.uid, claims, params: {} }));\n }\n\n return harden(await route.handler({ request, userId: null, claims: null, params: {} }));\n }\n\n async function route(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const path = routePath(url, deps.basePath);\n\n if (deps.onRequest) {\n const early = await deps.onRequest(request);\n if (early instanceof Response) return early;\n }\n\n if (deps.rateLimit) {\n const limited = deps.rateLimit(request);\n if (limited) return limited;\n }\n\n if (path === \"/cron\") {\n if (request.method !== \"POST\") return empty(405);\n return handleCron(request);\n }\n\n const pluginRoute = deps.pluginRoutes?.find(\n (candidate) => candidate.path === path && candidate.method === request.method,\n );\n if (pluginRoute) return handlePluginRoute(pluginRoute, request, url);\n\n if (request.method === \"POST\") {\n const rejected = csrfCheck(request, url, trustedOrigins);\n if (rejected) return rejected;\n }\n\n const userId = await resolveUserId(request);\n if (userId === \"error\") return empty(500);\n if (userId === null) return empty(401);\n\n if (request.method === \"GET\" && path === \"/events\") {\n if (!deps.events) return empty(404);\n // A session can open streams for free; without a cap one account can hold every socket.\n if ((openStreams.get(userId) ?? 0) >= deps.events.maxStreamsPerUser) {\n return json({ error: \"too many open event streams\" }, 429, { \"retry-after\": \"30\" });\n }\n if (totalStreams >= deps.events.maxStreams) {\n return json({ error: \"event streams unavailable\" }, 503, { \"retry-after\": \"30\" });\n }\n return events(request, userId, deps.events);\n }\n\n if (request.method === \"GET\" && path === \"/\") {\n const limitParam = Number(url.searchParams.get(\"limit\") ?? 20);\n const limit = Number.isFinite(limitParam) ? Math.min(Math.max(limitParam, 1), 100) : 20;\n const cursor = url.searchParams.get(\"cursor\");\n\n // unseenCount rides on the first page only: a cursor request is scrollback, not a poll.\n const [page, unseenCount] = await Promise.all([\n deps.adapter.listNotifications({\n userId,\n limit,\n ...(cursor ? { cursor } : {}),\n ...(url.searchParams.get(\"unreadOnly\") === \"true\" ? { unreadOnly: true } : {}),\n }),\n cursor ? Promise.resolve(null) : deps.adapter.countUnseen(userId),\n ]);\n\n return json(unseenCount === null ? page : { ...page, unseenCount });\n }\n\n if (request.method === \"GET\" && path === \"/count\") {\n return json({ unseen: await deps.adapter.countUnseen(userId) });\n }\n\n if (request.method === \"POST\" && path === \"/seen\") {\n const parsed = await readJsonBody(request, maxBodyBytes);\n if (\"error\" in parsed) return parsed.error;\n const before = typeof parsed.body.before === \"string\" ? new Date(parsed.body.before) : null;\n\n // +1ms: Postgres keeps microseconds, an ISO string milliseconds; without it markSeen misses the newest row.\n const cutoff =\n before && !Number.isNaN(before.getTime()) ? new Date(before.getTime() + 1) : new Date();\n\n await deps.adapter.markSeen(userId, cutoff);\n deps.signals.publish(inboxChannel(userId));\n return json({ ok: true });\n }\n\n if (request.method === \"POST\" && path === \"/read\") {\n const parsed = await readJsonBody(request, maxBodyBytes);\n if (\"error\" in parsed) return parsed.error;\n const ids = Array.isArray(parsed.body.ids)\n ? parsed.body.ids.filter((id): id is string => typeof id === \"string\")\n : [];\n if (ids.length === 0) return json({ error: \"ids must be a non-empty string array\" }, 400);\n\n if (ids.length > MAX_READ_IDS) {\n return json({ error: `at most ${MAX_READ_IDS} ids per request` }, 400);\n }\n\n // 404, not 403: a 403 confirms the row exists. RFC 0002 §2.\n const updated = await deps.adapter.markRead(userId, ids);\n if (updated > 0) deps.signals.publish(inboxChannel(userId));\n return updated === 0 ? empty(404) : json({ updated });\n }\n\n if (request.method === \"POST\" && path === \"/read-all\") {\n const updated = await deps.adapter.markAllRead(userId);\n if (updated > 0) deps.signals.publish(inboxChannel(userId));\n return json({ updated });\n }\n\n return empty(404);\n }\n\n const openStreams = new Map<string, number>();\n let totalStreams = 0;\n\n const trackStream = (userId: string) => {\n openStreams.set(userId, (openStreams.get(userId) ?? 0) + 1);\n totalStreams += 1;\n return () => {\n const remaining = (openStreams.get(userId) ?? 1) - 1;\n if (remaining > 0) openStreams.set(userId, remaining);\n else openStreams.delete(userId);\n totalStreams -= 1;\n };\n };\n\n /** A change fingerprint cheap enough to take every probe interval. */\n async function fingerprint(userId: string): Promise<string> {\n const [page, unseen] = await Promise.all([\n deps.adapter.listNotifications({ userId, limit: 1 }),\n deps.adapter.countUnseen(userId),\n ]);\n const newest = page.notifications[0];\n return `${newest?.id ?? \"\"}|${newest?.readAt ? 1 : 0}|${unseen}`;\n }\n\n // One probe per user, however many streams that user holds: N tabs must not mean N queries.\n type Probe = { listeners: Set<() => void>; timer: ReturnType<typeof setInterval>; last?: string };\n const probes = new Map<string, Probe>();\n\n function subscribeProbe(userId: string, onChanged: () => void, intervalMs: number) {\n let probe = probes.get(userId);\n if (!probe) {\n const created: Probe = {\n listeners: new Set(),\n timer: setInterval(() => {\n fingerprint(userId)\n .then((value) => {\n if (created.last !== undefined && value !== created.last) {\n for (const listener of created.listeners) listener();\n }\n created.last = value;\n })\n .catch((error) => deps.logger.error(\"events probe failed\", { error }));\n }, intervalMs),\n };\n probe = created;\n probes.set(userId, created);\n fingerprint(userId)\n .then((value) => {\n if (created.last === undefined) created.last = value;\n })\n .catch(() => {});\n }\n probe.listeners.add(onChanged);\n return () => {\n const current = probes.get(userId);\n if (!current) return;\n current.listeners.delete(onChanged);\n if (current.listeners.size === 0) {\n clearInterval(current.timer);\n probes.delete(userId);\n }\n };\n }\n\n /**\n * Server-Sent Events: `ready` once, `changed` whenever this user's inbox\n * moves, comments as keepalive. No payload ever travels on the stream; the\n * client fetches the feed on `changed`. RFC 0006 §4D.\n */\n function events(request: Request, userId: string, settings: EventsSettings): Response {\n const encoder = new TextEncoder();\n const timers: ReturnType<typeof setInterval>[] = [];\n const unsubscribes: (() => void)[] = [];\n let closed = false;\n let cleanup = () => {};\n\n const stream = new ReadableStream<Uint8Array>(\n {\n start(controller) {\n const write = (text: string) => {\n if (closed) return;\n // A consumer that stopped reading would make this queue grow without bound.\n if (controller.desiredSize !== null && controller.desiredSize <= 0) {\n cleanup();\n return;\n }\n try {\n controller.enqueue(encoder.encode(text));\n } catch {\n cleanup();\n }\n };\n cleanup = () => {\n if (closed) return;\n closed = true;\n for (const unsubscribe of unsubscribes) unsubscribe();\n for (const timer of timers) clearInterval(timer);\n try {\n controller.close();\n } catch {\n // already closed by the consumer\n }\n };\n const changed = () => write(\"event: changed\\ndata: {}\\n\\n\");\n\n unsubscribes.push(trackStream(userId));\n write(\"retry: 3000\\n\\nevent: ready\\ndata: {}\\n\\n\");\n unsubscribes.push(deps.signals.subscribe(inboxChannel(userId), changed));\n timers.push(setInterval(() => write(\": keepalive\\n\\n\"), settings.heartbeatMs));\n\n if (!deps.signals.crossProcess && settings.probeIntervalMs > 0) {\n unsubscribes.push(subscribeProbe(userId, changed, settings.probeIntervalMs));\n }\n\n if (settings.maxDurationMs > 0) {\n timers.push(\n setTimeout(cleanup, settings.maxDurationMs) as ReturnType<typeof setInterval>,\n );\n }\n request.signal?.addEventListener(\"abort\", cleanup, { once: true });\n },\n cancel() {\n cleanup();\n },\n },\n { highWaterMark: STREAM_HIGH_WATER_MARK },\n );\n\n return new Response(stream, {\n headers: {\n \"content-type\": \"text/event-stream; charset=utf-8\",\n \"cache-control\": \"no-cache, no-store\",\n connection: \"keep-alive\",\n \"x-accel-buffering\": \"no\",\n \"x-content-type-options\": \"nosniff\",\n },\n });\n }\n\n async function handle(request: Request): Promise<Response> {\n let response: Response | undefined;\n try {\n response = await route(request);\n return response;\n } catch (error) {\n // A thrown driver error must not escape: unhandled rejection on Node, stack trace elsewhere.\n deps.logger.error(\"request failed\", { url: request.url, error });\n return empty(500);\n } finally {\n // Only a served request earns a sweep; anonymous 401s must not drive database work.\n if (response && response.status < 400) deps.sweepOnRequest?.();\n }\n }\n\n return {\n handle,\n GET: handle,\n POST: handle,\n };\n}\n"]}
|
package/dist/client.cjs
CHANGED
|
@@ -21,12 +21,13 @@ function createNotifyClient(options = {}) {
|
|
|
21
21
|
const safetyNetMs = options.safetyNetMs ?? 5 * 6e4;
|
|
22
22
|
const activeWindowMs = options.activeWindowMs ?? 6e4;
|
|
23
23
|
const now = options.now ?? (() => Date.now());
|
|
24
|
+
const groupKey = options.scope ? `${baseUrl}#${options.scope}` : baseUrl;
|
|
24
25
|
const hasDom = typeof window !== "undefined" && typeof document !== "undefined";
|
|
25
26
|
const transport = options.transport ?? "auto";
|
|
26
27
|
const wantsStream = transport === "sse" || transport === "auto" && hasDom;
|
|
27
28
|
const isHidden = options.isDocumentHidden ?? (() => typeof document !== "undefined" && document.visibilityState === "hidden");
|
|
28
29
|
const locks = options.locks === void 0 ? typeof navigator !== "undefined" ? navigator.locks ?? null : null : options.locks;
|
|
29
|
-
const makeChannel = options.channel === void 0 ? typeof BroadcastChannel !== "undefined" ? () => new BroadcastChannel(`easy-ping:${
|
|
30
|
+
const makeChannel = options.channel === void 0 ? typeof BroadcastChannel !== "undefined" ? () => new BroadcastChannel(`easy-ping:${groupKey}`) : null : options.channel;
|
|
30
31
|
const serviceWorker = options.serviceWorker === void 0 ? typeof navigator !== "undefined" ? navigator.serviceWorker ?? null : null : options.serviceWorker;
|
|
31
32
|
const activityTarget = options.activityTarget === void 0 ? hasDom ? window : null : options.activityTarget;
|
|
32
33
|
let state = INITIAL;
|
|
@@ -335,7 +336,7 @@ function createNotifyClient(options = {}) {
|
|
|
335
336
|
becomeFollower();
|
|
336
337
|
lockAbort = new AbortController();
|
|
337
338
|
locks.request(
|
|
338
|
-
`easy-ping:leader:${
|
|
339
|
+
`easy-ping:leader:${groupKey}`,
|
|
339
340
|
{ mode: "exclusive", signal: lockAbort.signal },
|
|
340
341
|
() => new Promise((resolve) => {
|
|
341
342
|
releaseLock = resolve;
|
|
@@ -386,10 +387,18 @@ function createNotifyClient(options = {}) {
|
|
|
386
387
|
if (data?.source === "easy-ping" && data.type === "changed") changed();
|
|
387
388
|
}
|
|
388
389
|
let lastInboxVersion = null;
|
|
390
|
+
const sameOrigin = (response) => {
|
|
391
|
+
if (!response.url || typeof location === "undefined") return true;
|
|
392
|
+
try {
|
|
393
|
+
return new URL(response.url).origin === location.origin;
|
|
394
|
+
} catch {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
389
398
|
function instrument(inner) {
|
|
390
399
|
return (async (...args) => {
|
|
391
400
|
const response = await inner(...args);
|
|
392
|
-
const version = response.headers.get("x-easy-ping-inbox");
|
|
401
|
+
const version = sameOrigin(response) ? response.headers.get("x-easy-ping-inbox") : null;
|
|
393
402
|
if (version !== null && version !== lastInboxVersion) {
|
|
394
403
|
const first = lastInboxVersion === null;
|
|
395
404
|
lastInboxVersion = version;
|
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 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\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 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:${baseUrl}`) 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:${baseUrl}`,\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 /**\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 = response.headers.get(\"x-easy-ping-inbox\");\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 || !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"]}
|
package/dist/client.d.cts
CHANGED
|
@@ -77,6 +77,12 @@ type NotifyClientOptions = {
|
|
|
77
77
|
/** Where user activity is observed. Defaults to window; null disables. */
|
|
78
78
|
activityTarget?: MessageTargetLike | null | undefined;
|
|
79
79
|
now?: (() => number) | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Separates browsers' tab groups when identity is not a cookie: pass the
|
|
82
|
+
* current user's id (or session id) so tabs signed in as different users
|
|
83
|
+
* never share a leader or mirror each other's inbox.
|
|
84
|
+
*/
|
|
85
|
+
scope?: string | undefined;
|
|
80
86
|
};
|
|
81
87
|
declare function createNotifyClient(options?: NotifyClientOptions): {
|
|
82
88
|
subscribe: (listener: (state: NotifyState) => void) => () => void;
|
package/dist/client.d.ts
CHANGED
|
@@ -77,6 +77,12 @@ type NotifyClientOptions = {
|
|
|
77
77
|
/** Where user activity is observed. Defaults to window; null disables. */
|
|
78
78
|
activityTarget?: MessageTargetLike | null | undefined;
|
|
79
79
|
now?: (() => number) | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Separates browsers' tab groups when identity is not a cookie: pass the
|
|
82
|
+
* current user's id (or session id) so tabs signed in as different users
|
|
83
|
+
* never share a leader or mirror each other's inbox.
|
|
84
|
+
*/
|
|
85
|
+
scope?: string | undefined;
|
|
80
86
|
};
|
|
81
87
|
declare function createNotifyClient(options?: NotifyClientOptions): {
|
|
82
88
|
subscribe: (listener: (state: NotifyState) => void) => () => void;
|
package/dist/client.js
CHANGED
|
@@ -19,12 +19,13 @@ function createNotifyClient(options = {}) {
|
|
|
19
19
|
const safetyNetMs = options.safetyNetMs ?? 5 * 6e4;
|
|
20
20
|
const activeWindowMs = options.activeWindowMs ?? 6e4;
|
|
21
21
|
const now = options.now ?? (() => Date.now());
|
|
22
|
+
const groupKey = options.scope ? `${baseUrl}#${options.scope}` : baseUrl;
|
|
22
23
|
const hasDom = typeof window !== "undefined" && typeof document !== "undefined";
|
|
23
24
|
const transport = options.transport ?? "auto";
|
|
24
25
|
const wantsStream = transport === "sse" || transport === "auto" && hasDom;
|
|
25
26
|
const isHidden = options.isDocumentHidden ?? (() => typeof document !== "undefined" && document.visibilityState === "hidden");
|
|
26
27
|
const locks = options.locks === void 0 ? typeof navigator !== "undefined" ? navigator.locks ?? null : null : options.locks;
|
|
27
|
-
const makeChannel = options.channel === void 0 ? typeof BroadcastChannel !== "undefined" ? () => new BroadcastChannel(`easy-ping:${
|
|
28
|
+
const makeChannel = options.channel === void 0 ? typeof BroadcastChannel !== "undefined" ? () => new BroadcastChannel(`easy-ping:${groupKey}`) : null : options.channel;
|
|
28
29
|
const serviceWorker = options.serviceWorker === void 0 ? typeof navigator !== "undefined" ? navigator.serviceWorker ?? null : null : options.serviceWorker;
|
|
29
30
|
const activityTarget = options.activityTarget === void 0 ? hasDom ? window : null : options.activityTarget;
|
|
30
31
|
let state = INITIAL;
|
|
@@ -333,7 +334,7 @@ function createNotifyClient(options = {}) {
|
|
|
333
334
|
becomeFollower();
|
|
334
335
|
lockAbort = new AbortController();
|
|
335
336
|
locks.request(
|
|
336
|
-
`easy-ping:leader:${
|
|
337
|
+
`easy-ping:leader:${groupKey}`,
|
|
337
338
|
{ mode: "exclusive", signal: lockAbort.signal },
|
|
338
339
|
() => new Promise((resolve) => {
|
|
339
340
|
releaseLock = resolve;
|
|
@@ -384,10 +385,18 @@ function createNotifyClient(options = {}) {
|
|
|
384
385
|
if (data?.source === "easy-ping" && data.type === "changed") changed();
|
|
385
386
|
}
|
|
386
387
|
let lastInboxVersion = null;
|
|
388
|
+
const sameOrigin = (response) => {
|
|
389
|
+
if (!response.url || typeof location === "undefined") return true;
|
|
390
|
+
try {
|
|
391
|
+
return new URL(response.url).origin === location.origin;
|
|
392
|
+
} catch {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
387
396
|
function instrument(inner) {
|
|
388
397
|
return (async (...args) => {
|
|
389
398
|
const response = await inner(...args);
|
|
390
|
-
const version = response.headers.get("x-easy-ping-inbox");
|
|
399
|
+
const version = sameOrigin(response) ? response.headers.get("x-easy-ping-inbox") : null;
|
|
391
400
|
if (version !== null && version !== lastInboxVersion) {
|
|
392
401
|
const first = lastInboxVersion === null;
|
|
393
402
|
lastInboxVersion = version;
|