easy-ping 0.5.0 → 0.6.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 +548 -508
- package/dist/adapters/mongodb.cjs +142 -0
- package/dist/adapters/mongodb.cjs.map +1 -1
- package/dist/adapters/mongodb.d.cts +39 -1
- package/dist/adapters/mongodb.d.ts +39 -1
- package/dist/adapters/mongodb.js +73 -1
- package/dist/adapters/mongodb.js.map +1 -1
- package/dist/adapters/postgres.cjs +146 -0
- package/dist/adapters/postgres.cjs.map +1 -1
- package/dist/adapters/postgres.d.cts +36 -1
- package/dist/adapters/postgres.d.ts +36 -1
- package/dist/adapters/postgres.js +78 -3
- package/dist/adapters/postgres.js.map +1 -1
- package/dist/adapters/sqlite.js +1 -1
- package/dist/{chunk-GJNLOQJS.js → chunk-JKTMUMQK.js} +31 -6
- package/dist/chunk-JKTMUMQK.js.map +1 -0
- package/dist/chunk-ORZKH224.js +75 -0
- package/dist/chunk-ORZKH224.js.map +1 -0
- package/dist/{chunk-BECMTNK7.js → chunk-USQ6CQTJ.js} +88 -3
- package/dist/chunk-USQ6CQTJ.js.map +1 -0
- package/dist/client.cjs +288 -17
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +53 -1
- package/dist/client.d.ts +53 -1
- package/dist/client.js +288 -17
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +237 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -1
- package/dist/index.d.ts +42 -1
- package/dist/index.js +55 -7
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +26 -5
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +28 -6
- package/dist/node.js.map +1 -1
- package/dist/plugins/preferences.cjs.map +1 -1
- package/dist/plugins/preferences.js +2 -1
- package/dist/plugins/preferences.js.map +1 -1
- package/dist/plugins/push.cjs.map +1 -1
- package/dist/plugins/push.js +3 -2
- package/dist/plugins/push.js.map +1 -1
- package/dist/react.cjs +311 -19
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +52 -0
- package/dist/react.d.ts +52 -0
- package/dist/react.js +311 -19
- package/dist/react.js.map +1 -1
- package/dist/schema.js +1 -1
- package/dist/signals-kPmOlKDX.d.cts +27 -0
- package/dist/signals-kPmOlKDX.d.ts +27 -0
- package/dist/sw.cjs +39 -0
- package/dist/sw.cjs.map +1 -0
- package/dist/sw.d.cts +52 -0
- package/dist/sw.d.ts +52 -0
- package/dist/sw.js +36 -0
- package/dist/sw.js.map +1 -0
- package/package.json +6 -1
- package/dist/chunk-BECMTNK7.js.map +0 -1
- package/dist/chunk-GJNLOQJS.js.map +0 -1
package/dist/react.cjs
CHANGED
|
@@ -38,22 +38,62 @@ var INITIAL = {
|
|
|
38
38
|
error: null
|
|
39
39
|
};
|
|
40
40
|
var countUnread = (rows) => rows.reduce((total, row) => row.readAt === null ? total + 1 : total, 0);
|
|
41
|
+
var SHORT_STREAM_MS = 15e3;
|
|
42
|
+
var SHORT_STREAMS_BEFORE_GIVING_UP = 3;
|
|
41
43
|
function createNotifyClient(options = {}) {
|
|
42
44
|
const baseUrl = (options.baseUrl ?? "/api/notifications").replace(/\/$/, "");
|
|
43
45
|
const doFetch = options.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
44
46
|
const limit = options.limit ?? 20;
|
|
45
47
|
const pollIntervalMs = options.pollIntervalMs ?? 15e3;
|
|
46
|
-
const maxPollIntervalMs = options.maxPollIntervalMs ??
|
|
48
|
+
const maxPollIntervalMs = options.maxPollIntervalMs ?? 10 * 6e4;
|
|
49
|
+
const safetyNetMs = options.safetyNetMs ?? 5 * 6e4;
|
|
50
|
+
const activeWindowMs = options.activeWindowMs ?? 6e4;
|
|
51
|
+
const now = options.now ?? (() => Date.now());
|
|
52
|
+
const hasDom = typeof window !== "undefined" && typeof document !== "undefined";
|
|
53
|
+
const transport = options.transport ?? "auto";
|
|
54
|
+
const wantsStream = transport === "sse" || transport === "auto" && hasDom;
|
|
47
55
|
const isHidden = options.isDocumentHidden ?? (() => typeof document !== "undefined" && document.visibilityState === "hidden");
|
|
56
|
+
const locks = options.locks === void 0 ? typeof navigator !== "undefined" ? navigator.locks ?? null : null : options.locks;
|
|
57
|
+
const makeChannel = options.channel === void 0 ? typeof BroadcastChannel !== "undefined" ? () => new BroadcastChannel(`easy-ping:${baseUrl}`) : null : options.channel;
|
|
58
|
+
const serviceWorker = options.serviceWorker === void 0 ? typeof navigator !== "undefined" ? navigator.serviceWorker ?? null : null : options.serviceWorker;
|
|
59
|
+
const activityTarget = options.activityTarget === void 0 ? hasDom ? window : null : options.activityTarget;
|
|
48
60
|
let state = INITIAL;
|
|
49
61
|
const listeners = /* @__PURE__ */ new Set();
|
|
62
|
+
let role = "solo";
|
|
63
|
+
let running = false;
|
|
64
|
+
let channel;
|
|
65
|
+
let releaseLock;
|
|
66
|
+
let lockAbort;
|
|
50
67
|
let timer;
|
|
51
68
|
let consecutiveFailures = 0;
|
|
69
|
+
let idleTicks = 0;
|
|
70
|
+
let activeUntil = 0;
|
|
71
|
+
let lastRefreshAt = 0;
|
|
72
|
+
let refreshing;
|
|
73
|
+
let refreshAgain = false;
|
|
74
|
+
let streamAbort;
|
|
75
|
+
let streamConnected = false;
|
|
76
|
+
let streamDisabled = !wantsStream;
|
|
77
|
+
let shortStreams = 0;
|
|
78
|
+
let streamFailures = 0;
|
|
52
79
|
let generation = 0;
|
|
80
|
+
const localReads = /* @__PURE__ */ new Set();
|
|
81
|
+
let localSeen = false;
|
|
82
|
+
const stamp = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
83
|
+
function emit() {
|
|
84
|
+
for (const listener of listeners) listener(state);
|
|
85
|
+
}
|
|
53
86
|
function set(patch) {
|
|
54
87
|
state = { ...state, ...patch };
|
|
55
88
|
state = { ...state, unreadCount: countUnread(state.notifications) };
|
|
56
|
-
|
|
89
|
+
emit();
|
|
90
|
+
if (role === "leader") post({ type: "state", state });
|
|
91
|
+
}
|
|
92
|
+
function post(message) {
|
|
93
|
+
try {
|
|
94
|
+
channel?.postMessage(message);
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
57
97
|
}
|
|
58
98
|
async function request(path, init) {
|
|
59
99
|
const response = await doFetch(`${baseUrl}${path}`, {
|
|
@@ -64,7 +104,7 @@ function createNotifyClient(options = {}) {
|
|
|
64
104
|
throw new Error(`${init?.method ?? "GET"} ${path} failed: ${response.status}`);
|
|
65
105
|
return await response.json();
|
|
66
106
|
}
|
|
67
|
-
async function
|
|
107
|
+
async function fetchPage() {
|
|
68
108
|
const startedAt = generation;
|
|
69
109
|
const page = await request(`/?limit=${limit}`);
|
|
70
110
|
const unseenCount = page.unseenCount ?? (await request("/count")).unseen;
|
|
@@ -77,6 +117,24 @@ function createNotifyClient(options = {}) {
|
|
|
77
117
|
error: null
|
|
78
118
|
});
|
|
79
119
|
}
|
|
120
|
+
function refresh() {
|
|
121
|
+
if (refreshing) {
|
|
122
|
+
refreshAgain = true;
|
|
123
|
+
return refreshing;
|
|
124
|
+
}
|
|
125
|
+
refreshing = (async () => {
|
|
126
|
+
try {
|
|
127
|
+
do {
|
|
128
|
+
refreshAgain = false;
|
|
129
|
+
await fetchPage();
|
|
130
|
+
lastRefreshAt = now();
|
|
131
|
+
} while (refreshAgain);
|
|
132
|
+
} finally {
|
|
133
|
+
refreshing = void 0;
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
136
|
+
return refreshing;
|
|
137
|
+
}
|
|
80
138
|
let loadingMore;
|
|
81
139
|
async function loadMore() {
|
|
82
140
|
if (!state.nextCursor) return;
|
|
@@ -105,30 +163,39 @@ function createNotifyClient(options = {}) {
|
|
|
105
163
|
set({ notifications: apply(state.notifications), ...patch });
|
|
106
164
|
try {
|
|
107
165
|
await send();
|
|
166
|
+
if (role === "follower") post({ type: "mutated" });
|
|
108
167
|
} catch (error) {
|
|
109
168
|
generation += 1;
|
|
110
169
|
state = previous;
|
|
111
|
-
|
|
170
|
+
emit();
|
|
112
171
|
throw error;
|
|
113
172
|
}
|
|
114
173
|
}
|
|
115
|
-
const stamp = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
116
174
|
async function markAsRead(id) {
|
|
175
|
+
if (role === "follower") localReads.add(id);
|
|
117
176
|
await optimistic(
|
|
118
177
|
(rows) => rows.map((row) => row.id === id ? { ...row, readAt: row.readAt ?? stamp() } : row),
|
|
119
178
|
{},
|
|
120
179
|
() => request("/read", { method: "POST", body: JSON.stringify({ ids: [id] }) })
|
|
121
|
-
)
|
|
180
|
+
).catch((error) => {
|
|
181
|
+
localReads.delete(id);
|
|
182
|
+
throw error;
|
|
183
|
+
});
|
|
122
184
|
}
|
|
123
185
|
async function markAllRead() {
|
|
186
|
+
if (role === "follower") for (const row of state.notifications) localReads.add(row.id);
|
|
124
187
|
await optimistic(
|
|
125
188
|
(rows) => rows.map((row) => ({ ...row, readAt: row.readAt ?? stamp() })),
|
|
126
189
|
{},
|
|
127
190
|
() => request("/read-all", { method: "POST" })
|
|
128
|
-
)
|
|
191
|
+
).catch((error) => {
|
|
192
|
+
localReads.clear();
|
|
193
|
+
throw error;
|
|
194
|
+
});
|
|
129
195
|
}
|
|
130
196
|
async function markSeen() {
|
|
131
197
|
const newest = state.notifications[0]?.createdAt;
|
|
198
|
+
if (role === "follower") localSeen = true;
|
|
132
199
|
await optimistic(
|
|
133
200
|
(rows) => rows.map((row) => ({ ...row, seenAt: row.seenAt ?? stamp() })),
|
|
134
201
|
{ unseenCount: 0 },
|
|
@@ -136,13 +203,55 @@ function createNotifyClient(options = {}) {
|
|
|
136
203
|
method: "POST",
|
|
137
204
|
body: JSON.stringify(newest ? { before: newest } : {})
|
|
138
205
|
})
|
|
139
|
-
)
|
|
206
|
+
).catch((error) => {
|
|
207
|
+
localSeen = false;
|
|
208
|
+
throw error;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function applyRemoteState(remote) {
|
|
212
|
+
const notifications = remote.notifications.map((row) => {
|
|
213
|
+
if (!localReads.has(row.id)) return row;
|
|
214
|
+
if (row.readAt) {
|
|
215
|
+
localReads.delete(row.id);
|
|
216
|
+
return row;
|
|
217
|
+
}
|
|
218
|
+
return { ...row, readAt: stamp() };
|
|
219
|
+
});
|
|
220
|
+
if (localSeen && remote.unseenCount === 0) localSeen = false;
|
|
221
|
+
state = {
|
|
222
|
+
...remote,
|
|
223
|
+
notifications,
|
|
224
|
+
unseenCount: localSeen ? 0 : remote.unseenCount,
|
|
225
|
+
unreadCount: countUnread(notifications)
|
|
226
|
+
};
|
|
227
|
+
emit();
|
|
228
|
+
}
|
|
229
|
+
function onTabMessage(event) {
|
|
230
|
+
const message = event.data;
|
|
231
|
+
if (!message || typeof message !== "object") return;
|
|
232
|
+
if (role === "leader") {
|
|
233
|
+
if (message.type === "hello") post({ type: "state", state });
|
|
234
|
+
else if (message.type === "mutated") void refresh().catch(() => {
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (message.type === "state") applyRemoteState(message.state);
|
|
140
239
|
}
|
|
141
240
|
function schedule(delay) {
|
|
142
241
|
clearTimeout(timer);
|
|
143
242
|
timer = setTimeout(tick, delay);
|
|
144
243
|
}
|
|
244
|
+
function nextDelay() {
|
|
245
|
+
if (consecutiveFailures > 0) {
|
|
246
|
+
return Math.min(pollIntervalMs * 2 ** consecutiveFailures, maxPollIntervalMs);
|
|
247
|
+
}
|
|
248
|
+
if (streamConnected) return safetyNetMs;
|
|
249
|
+
if (now() < activeUntil) return pollIntervalMs;
|
|
250
|
+
idleTicks += 1;
|
|
251
|
+
return Math.min(pollIntervalMs * 2 ** idleTicks, maxPollIntervalMs);
|
|
252
|
+
}
|
|
145
253
|
async function tick() {
|
|
254
|
+
if (!running) return;
|
|
146
255
|
if (isHidden()) {
|
|
147
256
|
schedule(pollIntervalMs);
|
|
148
257
|
return;
|
|
@@ -154,20 +263,176 @@ function createNotifyClient(options = {}) {
|
|
|
154
263
|
consecutiveFailures += 1;
|
|
155
264
|
set({ isLoading: false, error: error instanceof Error ? error : new Error(String(error)) });
|
|
156
265
|
}
|
|
157
|
-
schedule(
|
|
158
|
-
|
|
159
|
-
|
|
266
|
+
if (running) schedule(nextDelay());
|
|
267
|
+
}
|
|
268
|
+
function onActivity() {
|
|
269
|
+
if (!running || role === "follower") return;
|
|
270
|
+
activeUntil = now() + activeWindowMs;
|
|
271
|
+
idleTicks = 0;
|
|
272
|
+
if (!isHidden() && now() - lastRefreshAt > pollIntervalMs) {
|
|
273
|
+
void refresh().catch(() => {
|
|
274
|
+
});
|
|
275
|
+
schedule(nextDelay());
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const onVisibility = () => {
|
|
279
|
+
if (!isHidden()) onActivity();
|
|
280
|
+
};
|
|
281
|
+
const ACTIVITY_EVENTS = ["focus", "pointerdown", "keydown", "online"];
|
|
282
|
+
function changed() {
|
|
283
|
+
if (!running) return;
|
|
284
|
+
void refresh().catch(() => {
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async function readStream(signal) {
|
|
288
|
+
const openedAt = now();
|
|
289
|
+
const response = await doFetch(`${baseUrl}/events`, {
|
|
290
|
+
headers: { accept: "text/event-stream" },
|
|
291
|
+
signal
|
|
292
|
+
});
|
|
293
|
+
if (!response.ok || !response.body) throw new Error(`GET /events failed: ${response.status}`);
|
|
294
|
+
const reader = response.body.getReader();
|
|
295
|
+
const decoder = new TextDecoder();
|
|
296
|
+
let buffer = "";
|
|
297
|
+
let eventName = "";
|
|
298
|
+
try {
|
|
299
|
+
for (; ; ) {
|
|
300
|
+
const { done, value } = await reader.read();
|
|
301
|
+
if (done) break;
|
|
302
|
+
buffer += decoder.decode(value, { stream: true });
|
|
303
|
+
let newline = buffer.indexOf("\n");
|
|
304
|
+
while (newline >= 0) {
|
|
305
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
306
|
+
buffer = buffer.slice(newline + 1);
|
|
307
|
+
if (line === "") {
|
|
308
|
+
if (eventName === "ready") {
|
|
309
|
+
streamConnected = true;
|
|
310
|
+
streamFailures = 0;
|
|
311
|
+
schedule(nextDelay());
|
|
312
|
+
} else if (eventName === "changed") changed();
|
|
313
|
+
eventName = "";
|
|
314
|
+
} else if (line.startsWith("event:")) eventName = line.slice(6).trim();
|
|
315
|
+
newline = buffer.indexOf("\n");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} finally {
|
|
319
|
+
streamConnected = false;
|
|
320
|
+
if (now() - openedAt < SHORT_STREAM_MS) shortStreams += 1;
|
|
321
|
+
else shortStreams = 0;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const isFollower = () => role === "follower";
|
|
325
|
+
async function streamLoop() {
|
|
326
|
+
while (running && !streamDisabled && !isFollower()) {
|
|
327
|
+
streamAbort = new AbortController();
|
|
328
|
+
try {
|
|
329
|
+
await readStream(streamAbort.signal);
|
|
330
|
+
} catch {
|
|
331
|
+
if (!running) return;
|
|
332
|
+
streamFailures += 1;
|
|
333
|
+
}
|
|
334
|
+
if (!running || isFollower()) return;
|
|
335
|
+
if (shortStreams >= SHORT_STREAMS_BEFORE_GIVING_UP) {
|
|
336
|
+
streamDisabled = true;
|
|
337
|
+
schedule(nextDelay());
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
await new Promise(
|
|
341
|
+
(resolve) => setTimeout(resolve, Math.min(3e3 * 2 ** streamFailures, 6e4))
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function becomeLeader() {
|
|
346
|
+
role = "leader";
|
|
347
|
+
post({ type: "leader" });
|
|
348
|
+
post({ type: "state", state });
|
|
349
|
+
void tick();
|
|
350
|
+
if (!streamDisabled) void streamLoop();
|
|
351
|
+
}
|
|
352
|
+
function becomeFollower() {
|
|
353
|
+
role = "follower";
|
|
354
|
+
post({ type: "hello" });
|
|
355
|
+
}
|
|
356
|
+
function acquireLeadership() {
|
|
357
|
+
if (!locks || !channel) {
|
|
358
|
+
role = "solo";
|
|
359
|
+
void tick();
|
|
360
|
+
if (!streamDisabled) void streamLoop();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
becomeFollower();
|
|
364
|
+
lockAbort = new AbortController();
|
|
365
|
+
locks.request(
|
|
366
|
+
`easy-ping:leader:${baseUrl}`,
|
|
367
|
+
{ mode: "exclusive", signal: lockAbort.signal },
|
|
368
|
+
() => new Promise((resolve) => {
|
|
369
|
+
releaseLock = resolve;
|
|
370
|
+
if (running) becomeLeader();
|
|
371
|
+
else resolve();
|
|
372
|
+
})
|
|
373
|
+
).catch(() => {
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
function start() {
|
|
377
|
+
running = true;
|
|
378
|
+
channel = makeChannel ? makeChannel() : void 0;
|
|
379
|
+
channel?.addEventListener("message", onTabMessage);
|
|
380
|
+
serviceWorker?.addEventListener("message", onRelay);
|
|
381
|
+
if (activityTarget) {
|
|
382
|
+
for (const name of ACTIVITY_EVENTS) activityTarget.addEventListener(name, onActivity);
|
|
383
|
+
}
|
|
384
|
+
if (typeof document !== "undefined") {
|
|
385
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
386
|
+
}
|
|
387
|
+
activeUntil = now() + activeWindowMs;
|
|
388
|
+
acquireLeadership();
|
|
389
|
+
}
|
|
390
|
+
function stop() {
|
|
391
|
+
running = false;
|
|
392
|
+
clearTimeout(timer);
|
|
393
|
+
timer = void 0;
|
|
394
|
+
streamAbort?.abort();
|
|
395
|
+
streamAbort = void 0;
|
|
396
|
+
lockAbort?.abort();
|
|
397
|
+
lockAbort = void 0;
|
|
398
|
+
releaseLock?.();
|
|
399
|
+
releaseLock = void 0;
|
|
400
|
+
channel?.removeEventListener("message", onTabMessage);
|
|
401
|
+
channel?.close();
|
|
402
|
+
channel = void 0;
|
|
403
|
+
serviceWorker?.removeEventListener("message", onRelay);
|
|
404
|
+
if (activityTarget) {
|
|
405
|
+
for (const name of ACTIVITY_EVENTS) activityTarget.removeEventListener(name, onActivity);
|
|
406
|
+
}
|
|
407
|
+
if (typeof document !== "undefined") {
|
|
408
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
409
|
+
}
|
|
410
|
+
role = "solo";
|
|
411
|
+
}
|
|
412
|
+
function onRelay(event) {
|
|
413
|
+
const data = event.data;
|
|
414
|
+
if (data?.source === "easy-ping" && data.type === "changed") changed();
|
|
415
|
+
}
|
|
416
|
+
let lastInboxVersion = null;
|
|
417
|
+
function instrument(inner) {
|
|
418
|
+
return (async (...args) => {
|
|
419
|
+
const response = await inner(...args);
|
|
420
|
+
const version = response.headers.get("x-easy-ping-inbox");
|
|
421
|
+
if (version !== null && version !== lastInboxVersion) {
|
|
422
|
+
const first = lastInboxVersion === null;
|
|
423
|
+
lastInboxVersion = version;
|
|
424
|
+
if (!first && role !== "follower") changed();
|
|
425
|
+
}
|
|
426
|
+
return response;
|
|
427
|
+
});
|
|
160
428
|
}
|
|
161
429
|
function subscribe(listener) {
|
|
162
430
|
listeners.add(listener);
|
|
163
431
|
listener(state);
|
|
164
|
-
if (listeners.size === 1)
|
|
432
|
+
if (listeners.size === 1) start();
|
|
165
433
|
return () => {
|
|
166
434
|
listeners.delete(listener);
|
|
167
|
-
if (listeners.size === 0)
|
|
168
|
-
clearTimeout(timer);
|
|
169
|
-
timer = void 0;
|
|
170
|
-
}
|
|
435
|
+
if (listeners.size === 0) stop();
|
|
171
436
|
};
|
|
172
437
|
}
|
|
173
438
|
return {
|
|
@@ -177,13 +442,28 @@ function createNotifyClient(options = {}) {
|
|
|
177
442
|
markAsRead,
|
|
178
443
|
markAllRead,
|
|
179
444
|
markSeen,
|
|
180
|
-
|
|
445
|
+
instrument,
|
|
446
|
+
getState: () => state,
|
|
447
|
+
getTransport: () => ({
|
|
448
|
+
role,
|
|
449
|
+
transport: streamDisabled ? "poll" : "sse",
|
|
450
|
+
connected: streamConnected
|
|
451
|
+
})
|
|
181
452
|
};
|
|
182
453
|
}
|
|
183
454
|
|
|
184
455
|
// src/react/index.ts
|
|
185
456
|
function useNotifications(options = {}) {
|
|
186
|
-
const {
|
|
457
|
+
const {
|
|
458
|
+
client: provided,
|
|
459
|
+
baseUrl,
|
|
460
|
+
limit,
|
|
461
|
+
pollIntervalMs,
|
|
462
|
+
maxPollIntervalMs,
|
|
463
|
+
safetyNetMs,
|
|
464
|
+
activeWindowMs,
|
|
465
|
+
transport
|
|
466
|
+
} = options;
|
|
187
467
|
const callbacksRef = (0, import_react.useRef)(options);
|
|
188
468
|
callbacksRef.current = options;
|
|
189
469
|
const client = (0, import_react.useMemo)(
|
|
@@ -192,10 +472,22 @@ function useNotifications(options = {}) {
|
|
|
192
472
|
limit,
|
|
193
473
|
pollIntervalMs,
|
|
194
474
|
maxPollIntervalMs,
|
|
475
|
+
safetyNetMs,
|
|
476
|
+
activeWindowMs,
|
|
477
|
+
transport,
|
|
195
478
|
fetch: (...args) => (callbacksRef.current.fetch ?? globalThis.fetch)(...args),
|
|
196
479
|
isDocumentHidden: () => callbacksRef.current.isDocumentHidden?.() ?? (typeof document !== "undefined" && document.visibilityState === "hidden")
|
|
197
480
|
}),
|
|
198
|
-
[
|
|
481
|
+
[
|
|
482
|
+
provided,
|
|
483
|
+
baseUrl,
|
|
484
|
+
limit,
|
|
485
|
+
pollIntervalMs,
|
|
486
|
+
maxPollIntervalMs,
|
|
487
|
+
safetyNetMs,
|
|
488
|
+
activeWindowMs,
|
|
489
|
+
transport
|
|
490
|
+
]
|
|
199
491
|
);
|
|
200
492
|
const [state, setState] = (0, import_react.useState)(() => client.getState());
|
|
201
493
|
(0, import_react.useEffect)(() => client.subscribe(setState), [client]);
|
package/dist/react.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react/index.ts","../src/client/index.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n createNotifyClient,\n type NotifyClient,\n type NotifyClientOptions,\n type NotifyState,\n} from \"../client\";\n\nexport type UseNotificationsResult = NotifyState & {\n markAsRead: (id: string) => Promise<void>;\n markAllRead: () => Promise<void>;\n markSeen: () => Promise<void>;\n loadMore: () => Promise<void>;\n refresh: () => Promise<void>;\n};\n\n/** State lives in the framework-agnostic client; this only wires React's lifecycle. */\nexport function useNotifications(\n options: NotifyClientOptions & { client?: NotifyClient } = {},\n): UseNotificationsResult {\n const { client: provided, baseUrl, limit, pollIntervalMs, maxPollIntervalMs } = options;\n\n // Held in a ref so a new callback identity per render does not rebuild the client.\n const callbacksRef = useRef(options);\n callbacksRef.current = options;\n\n const client = useMemo(\n () =>\n provided ??\n createNotifyClient({\n baseUrl,\n limit,\n pollIntervalMs,\n maxPollIntervalMs,\n fetch: (...args) => (callbacksRef.current.fetch ?? globalThis.fetch)(...args),\n isDocumentHidden: () =>\n callbacksRef.current.isDocumentHidden?.() ??\n (typeof document !== \"undefined\" && document.visibilityState === \"hidden\"),\n }),\n [provided, baseUrl, limit, pollIntervalMs, maxPollIntervalMs],\n );\n\n const [state, setState] = useState<NotifyState>(() => client.getState());\n\n useEffect(() => client.subscribe(setState), [client]);\n\n return {\n ...state,\n markAsRead: useCallback((id: string) => client.markAsRead(id), [client]),\n markAllRead: useCallback(() => client.markAllRead(), [client]),\n markSeen: useCallback(() => client.markSeen(), [client]),\n loadMore: useCallback(() => client.loadMore(), [client]),\n refresh: useCallback(() => client.refresh(), [client]),\n };\n}\n\nexport type {\n NotificationView,\n NotifyClient,\n NotifyClientOptions,\n NotifyState,\n} from \"../client\";\nexport { createNotifyClient } from \"../client\";\n","export type NotificationView = {\n id: string;\n userId: string;\n type: string;\n payload: unknown;\n actorId: string | null;\n groupKey: string | null;\n seenAt: string | null;\n readAt: string | null;\n createdAt: string;\n};\n\nexport type NotifyState = {\n notifications: readonly NotificationView[];\n /** Badge count — cleared when the dropdown opens. Server-authoritative. */\n unseenCount: number;\n /** Items still bold. Derived from what is loaded, not a server total. */\n unreadCount: number;\n nextCursor: string | null;\n isLoading: boolean;\n error: Error | null;\n};\n\nexport type NotifyClientOptions = {\n baseUrl?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n limit?: number | undefined;\n pollIntervalMs?: number | undefined;\n maxPollIntervalMs?: number | undefined;\n /** Injectable so the poller can be tested without a DOM. */\n isDocumentHidden?: (() => boolean) | undefined;\n};\n\nconst INITIAL: NotifyState = {\n notifications: [],\n unseenCount: 0,\n unreadCount: 0,\n nextCursor: null,\n isLoading: true,\n error: null,\n};\n\nconst countUnread = (rows: readonly NotificationView[]) =>\n rows.reduce((total, row) => (row.readAt === null ? total + 1 : total), 0);\n\nexport function createNotifyClient(options: NotifyClientOptions = {}) {\n const baseUrl = (options.baseUrl ?? \"/api/notifications\").replace(/\\/$/, \"\");\n const doFetch = options.fetch ?? ((...args) => globalThis.fetch(...args));\n const limit = options.limit ?? 20;\n const pollIntervalMs = options.pollIntervalMs ?? 15_000;\n const maxPollIntervalMs = options.maxPollIntervalMs ?? 5 * 60_000;\n const isHidden =\n options.isDocumentHidden ??\n (() => typeof document !== \"undefined\" && document.visibilityState === \"hidden\");\n\n let state: NotifyState = INITIAL;\n const listeners = new Set<(state: NotifyState) => void>();\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let consecutiveFailures = 0;\n\n // Bumped per mutation so an in-flight poll cannot revert an optimistic update.\n let generation = 0;\n\n function set(patch: Partial<NotifyState>) {\n state = { ...state, ...patch };\n state = { ...state, unreadCount: countUnread(state.notifications) };\n for (const listener of listeners) listener(state);\n }\n\n async function request<T>(path: string, init?: RequestInit): Promise<T> {\n const response = await doFetch(`${baseUrl}${path}`, {\n ...init,\n headers: { \"content-type\": \"application/json\", ...init?.headers },\n });\n if (!response.ok)\n throw new Error(`${init?.method ?? \"GET\"} ${path} failed: ${response.status}`);\n return (await response.json()) as T;\n }\n\n async function refresh(): Promise<void> {\n const startedAt = generation;\n\n // One request per poll; /count is only the fallback for a server too old to send unseenCount.\n const page = await request<{\n notifications: NotificationView[];\n nextCursor: string | null;\n unseenCount?: number;\n }>(`/?limit=${limit}`);\n\n const unseenCount = page.unseenCount ?? (await request<{ unseen: number }>(\"/count\")).unseen;\n\n // A mutation landed while this was in flight; the response is stale.\n if (generation !== startedAt) return;\n\n set({\n notifications: page.notifications,\n nextCursor: page.nextCursor,\n unseenCount,\n isLoading: false,\n error: null,\n });\n }\n\n let loadingMore: Promise<void> | undefined;\n\n async function loadMore(): Promise<void> {\n if (!state.nextCursor) return;\n\n // Share the in-flight request so two clicks cannot append the same page twice.\n if (loadingMore) return loadingMore;\n\n const cursor = state.nextCursor;\n\n loadingMore = (async () => {\n try {\n const page = await request<{\n notifications: NotificationView[];\n nextCursor: string | null;\n }>(`/?limit=${limit}&cursor=${encodeURIComponent(cursor)}`);\n\n const known = new Set(state.notifications.map((row) => row.id));\n\n set({\n notifications: [\n ...state.notifications,\n ...page.notifications.filter((row) => !known.has(row.id)),\n ],\n nextCursor: page.nextCursor,\n });\n } finally {\n loadingMore = undefined;\n }\n })();\n\n return loadingMore;\n }\n\n /** Applies a patch immediately and reverts it if the request fails. */\n async function optimistic(\n apply: (rows: readonly NotificationView[]) => readonly NotificationView[],\n patch: Partial<NotifyState>,\n send: () => Promise<unknown>,\n ): Promise<void> {\n const previous = state;\n generation += 1;\n set({ notifications: apply(state.notifications), ...patch });\n\n try {\n await send();\n } catch (error) {\n generation += 1;\n state = previous;\n for (const listener of listeners) listener(state);\n throw error;\n }\n }\n\n const stamp = () => new Date().toISOString();\n\n async function markAsRead(id: string): Promise<void> {\n await optimistic(\n (rows) =>\n rows.map((row) => (row.id === id ? { ...row, readAt: row.readAt ?? stamp() } : row)),\n {},\n () => request(\"/read\", { method: \"POST\", body: JSON.stringify({ ids: [id] }) }),\n );\n }\n\n async function markAllRead(): Promise<void> {\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, readAt: row.readAt ?? stamp() })),\n {},\n () => request(\"/read-all\", { method: \"POST\" }),\n );\n }\n\n async function markSeen(): Promise<void> {\n // Only what the user could have seen: no cutoff would mark newer arrivals seen too.\n const newest = state.notifications[0]?.createdAt;\n\n await optimistic(\n (rows) => rows.map((row) => ({ ...row, seenAt: row.seenAt ?? stamp() })),\n { unseenCount: 0 },\n () =>\n request(\"/seen\", {\n method: \"POST\",\n body: JSON.stringify(newest ? { before: newest } : {}),\n }),\n );\n }\n\n function schedule(delay: number) {\n clearTimeout(timer);\n timer = setTimeout(tick, delay);\n }\n\n async function tick() {\n // A hidden tab is not looking at the bell.\n if (isHidden()) {\n schedule(pollIntervalMs);\n return;\n }\n\n try {\n await refresh();\n consecutiveFailures = 0;\n } catch (error) {\n consecutiveFailures += 1;\n set({ isLoading: false, error: error instanceof Error ? error : new Error(String(error)) });\n }\n\n // Back off on failure so an outage does not become a retry storm.\n schedule(\n consecutiveFailures === 0\n ? pollIntervalMs\n : Math.min(pollIntervalMs * 2 ** consecutiveFailures, maxPollIntervalMs),\n );\n }\n\n function subscribe(listener: (state: NotifyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n\n // One poller regardless of how many components subscribe.\n if (listeners.size === 1) void tick();\n\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n }\n\n return {\n subscribe,\n refresh,\n loadMore,\n markAsRead,\n markAllRead,\n markSeen,\n getState: () => state,\n };\n}\n\nexport type NotifyClient = ReturnType<typeof createNotifyClient>;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/react/index.ts","../src/client/index.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n createNotifyClient,\n type NotifyClient,\n type NotifyClientOptions,\n type NotifyState,\n} from \"../client\";\n\nexport type UseNotificationsResult = NotifyState & {\n markAsRead: (id: string) => Promise<void>;\n markAllRead: () => Promise<void>;\n markSeen: () => Promise<void>;\n loadMore: () => Promise<void>;\n refresh: () => Promise<void>;\n};\n\n/** State lives in the framework-agnostic client; this only wires React's lifecycle. */\nexport function useNotifications(\n options: NotifyClientOptions & { client?: NotifyClient } = {},\n): UseNotificationsResult {\n const {\n client: provided,\n baseUrl,\n limit,\n pollIntervalMs,\n maxPollIntervalMs,\n safetyNetMs,\n activeWindowMs,\n transport,\n } = options;\n\n // Held in a ref so a new callback identity per render does not rebuild the client.\n const callbacksRef = useRef(options);\n callbacksRef.current = options;\n\n const client = useMemo(\n () =>\n provided ??\n createNotifyClient({\n baseUrl,\n limit,\n pollIntervalMs,\n maxPollIntervalMs,\n safetyNetMs,\n activeWindowMs,\n transport,\n fetch: (...args) => (callbacksRef.current.fetch ?? globalThis.fetch)(...args),\n isDocumentHidden: () =>\n callbacksRef.current.isDocumentHidden?.() ??\n (typeof document !== \"undefined\" && document.visibilityState === \"hidden\"),\n }),\n [\n provided,\n baseUrl,\n limit,\n pollIntervalMs,\n maxPollIntervalMs,\n safetyNetMs,\n activeWindowMs,\n transport,\n ],\n );\n\n const [state, setState] = useState<NotifyState>(() => client.getState());\n\n useEffect(() => client.subscribe(setState), [client]);\n\n return {\n ...state,\n markAsRead: useCallback((id: string) => client.markAsRead(id), [client]),\n markAllRead: useCallback(() => client.markAllRead(), [client]),\n markSeen: useCallback(() => client.markSeen(), [client]),\n loadMore: useCallback(() => client.loadMore(), [client]),\n refresh: useCallback(() => client.refresh(), [client]),\n };\n}\n\nexport type {\n NotificationView,\n NotifyClient,\n NotifyClientOptions,\n NotifyState,\n} from \"../client\";\nexport { createNotifyClient } from \"../client\";\n","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"],"mappings":"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","names":[]}
|
package/dist/react.d.cts
CHANGED
|
@@ -19,14 +19,64 @@ type NotifyState = {
|
|
|
19
19
|
isLoading: boolean;
|
|
20
20
|
error: Error | null;
|
|
21
21
|
};
|
|
22
|
+
type Transport = "auto" | "sse" | "poll";
|
|
23
|
+
type TransportStatus = {
|
|
24
|
+
/** "leader" holds this browser's single connection; "follower" mirrors it; "solo" has no Web Locks. */
|
|
25
|
+
role: "leader" | "follower" | "solo";
|
|
26
|
+
/** What the leader is using right now. */
|
|
27
|
+
transport: "sse" | "poll";
|
|
28
|
+
/** True while the event stream has said `ready` and not since closed. */
|
|
29
|
+
connected: boolean;
|
|
30
|
+
};
|
|
31
|
+
type LockManagerLike = {
|
|
32
|
+
request(name: string, options: {
|
|
33
|
+
mode: "exclusive";
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
}, callback: () => Promise<void>): Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
type ChannelLike = {
|
|
38
|
+
postMessage(message: unknown): void;
|
|
39
|
+
addEventListener(type: "message", handler: (event: {
|
|
40
|
+
data: unknown;
|
|
41
|
+
}) => void): void;
|
|
42
|
+
removeEventListener(type: "message", handler: (event: {
|
|
43
|
+
data: unknown;
|
|
44
|
+
}) => void): void;
|
|
45
|
+
close(): void;
|
|
46
|
+
};
|
|
47
|
+
type MessageTargetLike = {
|
|
48
|
+
addEventListener(type: string, handler: (event: {
|
|
49
|
+
data?: unknown;
|
|
50
|
+
}) => void): void;
|
|
51
|
+
removeEventListener(type: string, handler: (event: {
|
|
52
|
+
data?: unknown;
|
|
53
|
+
}) => void): void;
|
|
54
|
+
};
|
|
22
55
|
type NotifyClientOptions = {
|
|
23
56
|
baseUrl?: string | undefined;
|
|
24
57
|
fetch?: typeof globalThis.fetch | undefined;
|
|
25
58
|
limit?: number | undefined;
|
|
59
|
+
/** Poll interval while the user is active and no stream is connected. Default 15 s. */
|
|
26
60
|
pollIntervalMs?: number | undefined;
|
|
61
|
+
/** Ceiling the idle backoff grows to. Default 10 min. */
|
|
27
62
|
maxPollIntervalMs?: number | undefined;
|
|
63
|
+
/** Poll interval while the event stream is connected: a safety net. Default 5 min. */
|
|
64
|
+
safetyNetMs?: number | undefined;
|
|
65
|
+
/** How long after user input polling stays at the base interval. Default 60 s. */
|
|
66
|
+
activeWindowMs?: number | undefined;
|
|
67
|
+
/** "auto" streams when a DOM is present, "poll" never streams, "sse" always tries. */
|
|
68
|
+
transport?: Transport | undefined;
|
|
28
69
|
/** Injectable so the poller can be tested without a DOM. */
|
|
29
70
|
isDocumentHidden?: (() => boolean) | undefined;
|
|
71
|
+
/** Web Locks, for one connection per browser. Defaults to navigator.locks; null disables. */
|
|
72
|
+
locks?: LockManagerLike | null | undefined;
|
|
73
|
+
/** Cross-tab channel factory. Defaults to BroadcastChannel; null disables. */
|
|
74
|
+
channel?: (() => ChannelLike) | null | undefined;
|
|
75
|
+
/** Where push relays arrive. Defaults to navigator.serviceWorker; null disables. */
|
|
76
|
+
serviceWorker?: MessageTargetLike | null | undefined;
|
|
77
|
+
/** Where user activity is observed. Defaults to window; null disables. */
|
|
78
|
+
activityTarget?: MessageTargetLike | null | undefined;
|
|
79
|
+
now?: (() => number) | undefined;
|
|
30
80
|
};
|
|
31
81
|
declare function createNotifyClient(options?: NotifyClientOptions): {
|
|
32
82
|
subscribe: (listener: (state: NotifyState) => void) => () => void;
|
|
@@ -35,7 +85,9 @@ declare function createNotifyClient(options?: NotifyClientOptions): {
|
|
|
35
85
|
markAsRead: (id: string) => Promise<void>;
|
|
36
86
|
markAllRead: () => Promise<void>;
|
|
37
87
|
markSeen: () => Promise<void>;
|
|
88
|
+
instrument: (inner: typeof globalThis.fetch) => typeof globalThis.fetch;
|
|
38
89
|
getState: () => NotifyState;
|
|
90
|
+
getTransport: () => TransportStatus;
|
|
39
91
|
};
|
|
40
92
|
type NotifyClient = ReturnType<typeof createNotifyClient>;
|
|
41
93
|
|