easy-ping 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/{adapter-BJ8Iejvs.d.cts → adapter-CV5Yt2c6.d.cts} +1 -1
- package/dist/{adapter-BJ8Iejvs.d.ts → adapter-CV5Yt2c6.d.ts} +1 -1
- package/dist/adapters/drizzle.d.cts +1 -1
- package/dist/adapters/drizzle.d.ts +1 -1
- package/dist/adapters/mongodb.d.cts +2 -2
- package/dist/adapters/mongodb.d.ts +2 -2
- package/dist/adapters/mysql.d.cts +2 -2
- package/dist/adapters/mysql.d.ts +2 -2
- package/dist/adapters/postgres.d.cts +2 -2
- package/dist/adapters/postgres.d.ts +2 -2
- package/dist/adapters/sqlite.d.cts +2 -2
- package/dist/adapters/sqlite.d.ts +2 -2
- package/dist/client.cjs +7 -2
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +7 -2
- package/dist/client.js.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/{plugin-Wwp1nvo0.d.cts → plugin-6S8tpH0n.d.cts} +1 -1
- package/dist/{plugin-CSLKKgXT.d.ts → plugin-C4fikLl7.d.ts} +1 -1
- package/dist/plugins/digests.d.cts +2 -2
- package/dist/plugins/digests.d.ts +2 -2
- package/dist/plugins/mobile-push.cjs +307 -0
- package/dist/plugins/mobile-push.cjs.map +1 -0
- package/dist/plugins/mobile-push.d.cts +117 -0
- package/dist/plugins/mobile-push.d.ts +117 -0
- package/dist/plugins/mobile-push.js +261 -0
- package/dist/plugins/mobile-push.js.map +1 -0
- package/dist/plugins/preferences-client.d.cts +2 -2
- package/dist/plugins/preferences-client.d.ts +2 -2
- package/dist/plugins/preferences-react.d.cts +1 -1
- package/dist/plugins/preferences-react.d.ts +1 -1
- package/dist/plugins/preferences.cjs +9 -1
- package/dist/plugins/preferences.cjs.map +1 -1
- package/dist/plugins/preferences.d.cts +2 -2
- package/dist/plugins/preferences.d.ts +2 -2
- package/dist/plugins/preferences.js +9 -1
- package/dist/plugins/preferences.js.map +1 -1
- package/dist/plugins/push.d.cts +2 -2
- package/dist/plugins/push.d.ts +2 -2
- package/dist/plugins/telegram.d.cts +2 -2
- package/dist/plugins/telegram.d.ts +2 -2
- package/dist/providers/expo-push.cjs +114 -0
- package/dist/providers/expo-push.cjs.map +1 -0
- package/dist/providers/expo-push.d.cts +61 -0
- package/dist/providers/expo-push.d.ts +61 -0
- package/dist/providers/expo-push.js +109 -0
- package/dist/providers/expo-push.js.map +1 -0
- package/dist/providers/web-push.d.cts +2 -2
- package/dist/providers/web-push.d.ts +2 -2
- package/dist/react-native.cjs +637 -0
- package/dist/react-native.cjs.map +1 -0
- package/dist/react-native.d.cts +145 -0
- package/dist/react-native.d.ts +145 -0
- package/dist/react-native.js +609 -0
- package/dist/react-native.js.map +1 -0
- package/dist/react.cjs +7 -2
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +7 -2
- package/dist/react.js.map +1 -1
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/package.json +26 -3
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
// src/react-native/index.ts
|
|
2
|
+
import { useMemo as useMemo2, useRef as useRef2 } from "react";
|
|
3
|
+
import { AppState } from "react-native";
|
|
4
|
+
|
|
5
|
+
// src/client/index.ts
|
|
6
|
+
var INITIAL = {
|
|
7
|
+
notifications: [],
|
|
8
|
+
unseenCount: 0,
|
|
9
|
+
unreadCount: 0,
|
|
10
|
+
nextCursor: null,
|
|
11
|
+
isLoading: true,
|
|
12
|
+
error: null
|
|
13
|
+
};
|
|
14
|
+
var countUnread = (rows) => rows.reduce((total, row) => row.readAt === null ? total + 1 : total, 0);
|
|
15
|
+
var SHORT_STREAM_MS = 15e3;
|
|
16
|
+
var SHORT_STREAMS_BEFORE_GIVING_UP = 3;
|
|
17
|
+
function createNotifyClient(options = {}) {
|
|
18
|
+
const baseUrl = (options.baseUrl ?? "/api/notifications").replace(/\/$/, "");
|
|
19
|
+
const doFetch = options.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
20
|
+
const limit = options.limit ?? 20;
|
|
21
|
+
const pollIntervalMs = options.pollIntervalMs ?? 15e3;
|
|
22
|
+
const maxPollIntervalMs = options.maxPollIntervalMs ?? 10 * 6e4;
|
|
23
|
+
const safetyNetMs = options.safetyNetMs ?? 5 * 6e4;
|
|
24
|
+
const activeWindowMs = options.activeWindowMs ?? 6e4;
|
|
25
|
+
const now = options.now ?? (() => Date.now());
|
|
26
|
+
const groupKey = options.scope ? `${baseUrl}#${options.scope}` : baseUrl;
|
|
27
|
+
const hasDom = typeof window !== "undefined" && typeof document !== "undefined";
|
|
28
|
+
const transport = options.transport ?? "auto";
|
|
29
|
+
const wantsStream = transport === "sse" || transport === "auto" && hasDom;
|
|
30
|
+
const isHidden = options.isDocumentHidden ?? (() => typeof document !== "undefined" && document.visibilityState === "hidden");
|
|
31
|
+
const locks = options.locks === void 0 ? typeof navigator !== "undefined" ? navigator.locks ?? null : null : options.locks;
|
|
32
|
+
const makeChannel = options.channel === void 0 ? typeof BroadcastChannel !== "undefined" ? () => new BroadcastChannel(`easy-ping:${groupKey}`) : null : options.channel;
|
|
33
|
+
const serviceWorker = options.serviceWorker === void 0 ? typeof navigator !== "undefined" ? navigator.serviceWorker ?? null : null : options.serviceWorker;
|
|
34
|
+
const activityTarget = options.activityTarget === void 0 ? hasDom ? window : null : options.activityTarget;
|
|
35
|
+
let state = INITIAL;
|
|
36
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
37
|
+
let role = "solo";
|
|
38
|
+
let running = false;
|
|
39
|
+
let channel;
|
|
40
|
+
let releaseLock;
|
|
41
|
+
let lockAbort;
|
|
42
|
+
let timer;
|
|
43
|
+
let consecutiveFailures = 0;
|
|
44
|
+
let idleTicks = 0;
|
|
45
|
+
let activeUntil = 0;
|
|
46
|
+
let lastRefreshAt = 0;
|
|
47
|
+
let refreshing;
|
|
48
|
+
let refreshAgain = false;
|
|
49
|
+
let streamAbort;
|
|
50
|
+
let streamConnected = false;
|
|
51
|
+
let streamDisabled = !wantsStream;
|
|
52
|
+
let shortStreams = 0;
|
|
53
|
+
let streamFailures = 0;
|
|
54
|
+
let generation = 0;
|
|
55
|
+
const localReads = /* @__PURE__ */ new Set();
|
|
56
|
+
let localSeen = false;
|
|
57
|
+
const stamp = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
58
|
+
function emit() {
|
|
59
|
+
for (const listener of listeners) listener(state);
|
|
60
|
+
}
|
|
61
|
+
function set(patch) {
|
|
62
|
+
state = { ...state, ...patch };
|
|
63
|
+
state = { ...state, unreadCount: countUnread(state.notifications) };
|
|
64
|
+
emit();
|
|
65
|
+
if (role === "leader") post2({ type: "state", state });
|
|
66
|
+
}
|
|
67
|
+
function post2(message) {
|
|
68
|
+
try {
|
|
69
|
+
channel?.postMessage(message);
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function request(path, init) {
|
|
74
|
+
const response = await doFetch(`${baseUrl}${path}`, {
|
|
75
|
+
...init,
|
|
76
|
+
headers: { "content-type": "application/json", ...init?.headers }
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok)
|
|
79
|
+
throw new Error(`${init?.method ?? "GET"} ${path} failed: ${response.status}`);
|
|
80
|
+
return await response.json();
|
|
81
|
+
}
|
|
82
|
+
async function fetchPage() {
|
|
83
|
+
const startedAt = generation;
|
|
84
|
+
const page = await request(`/?limit=${limit}`);
|
|
85
|
+
const unseenCount = page.unseenCount ?? (await request("/count")).unseen;
|
|
86
|
+
if (generation !== startedAt) return;
|
|
87
|
+
set({
|
|
88
|
+
notifications: page.notifications,
|
|
89
|
+
nextCursor: page.nextCursor,
|
|
90
|
+
unseenCount,
|
|
91
|
+
isLoading: false,
|
|
92
|
+
error: null
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function refresh() {
|
|
96
|
+
if (refreshing) {
|
|
97
|
+
refreshAgain = true;
|
|
98
|
+
return refreshing;
|
|
99
|
+
}
|
|
100
|
+
refreshing = (async () => {
|
|
101
|
+
try {
|
|
102
|
+
do {
|
|
103
|
+
refreshAgain = false;
|
|
104
|
+
await fetchPage();
|
|
105
|
+
lastRefreshAt = now();
|
|
106
|
+
} while (refreshAgain);
|
|
107
|
+
} finally {
|
|
108
|
+
refreshing = void 0;
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
return refreshing;
|
|
112
|
+
}
|
|
113
|
+
let loadingMore;
|
|
114
|
+
async function loadMore() {
|
|
115
|
+
if (!state.nextCursor) return;
|
|
116
|
+
if (loadingMore) return loadingMore;
|
|
117
|
+
const cursor = state.nextCursor;
|
|
118
|
+
loadingMore = (async () => {
|
|
119
|
+
try {
|
|
120
|
+
const page = await request(`/?limit=${limit}&cursor=${encodeURIComponent(cursor)}`);
|
|
121
|
+
const known = new Set(state.notifications.map((row) => row.id));
|
|
122
|
+
set({
|
|
123
|
+
notifications: [
|
|
124
|
+
...state.notifications,
|
|
125
|
+
...page.notifications.filter((row) => !known.has(row.id))
|
|
126
|
+
],
|
|
127
|
+
nextCursor: page.nextCursor
|
|
128
|
+
});
|
|
129
|
+
} finally {
|
|
130
|
+
loadingMore = void 0;
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
133
|
+
return loadingMore;
|
|
134
|
+
}
|
|
135
|
+
async function optimistic(apply, patch, send) {
|
|
136
|
+
const previous = state;
|
|
137
|
+
generation += 1;
|
|
138
|
+
set({ notifications: apply(state.notifications), ...patch });
|
|
139
|
+
try {
|
|
140
|
+
await send();
|
|
141
|
+
if (role === "follower") post2({ type: "mutated" });
|
|
142
|
+
} catch (error) {
|
|
143
|
+
generation += 1;
|
|
144
|
+
state = previous;
|
|
145
|
+
emit();
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function markAsRead(id) {
|
|
150
|
+
if (role === "follower") localReads.add(id);
|
|
151
|
+
await optimistic(
|
|
152
|
+
(rows) => rows.map((row) => row.id === id ? { ...row, readAt: row.readAt ?? stamp() } : row),
|
|
153
|
+
{},
|
|
154
|
+
() => request("/read", { method: "POST", body: JSON.stringify({ ids: [id] }) })
|
|
155
|
+
).catch((error) => {
|
|
156
|
+
localReads.delete(id);
|
|
157
|
+
throw error;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async function markAllRead() {
|
|
161
|
+
if (role === "follower") for (const row of state.notifications) localReads.add(row.id);
|
|
162
|
+
await optimistic(
|
|
163
|
+
(rows) => rows.map((row) => ({ ...row, readAt: row.readAt ?? stamp() })),
|
|
164
|
+
{},
|
|
165
|
+
() => request("/read-all", { method: "POST" })
|
|
166
|
+
).catch((error) => {
|
|
167
|
+
localReads.clear();
|
|
168
|
+
throw error;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async function markSeen() {
|
|
172
|
+
const newest = state.notifications[0]?.createdAt;
|
|
173
|
+
if (role === "follower") localSeen = true;
|
|
174
|
+
await optimistic(
|
|
175
|
+
(rows) => rows.map((row) => ({ ...row, seenAt: row.seenAt ?? stamp() })),
|
|
176
|
+
{ unseenCount: 0 },
|
|
177
|
+
() => request("/seen", {
|
|
178
|
+
method: "POST",
|
|
179
|
+
body: JSON.stringify(newest ? { before: newest } : {})
|
|
180
|
+
})
|
|
181
|
+
).catch((error) => {
|
|
182
|
+
localSeen = false;
|
|
183
|
+
throw error;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function applyRemoteState(remote) {
|
|
187
|
+
const notifications = remote.notifications.map((row) => {
|
|
188
|
+
if (!localReads.has(row.id)) return row;
|
|
189
|
+
if (row.readAt) {
|
|
190
|
+
localReads.delete(row.id);
|
|
191
|
+
return row;
|
|
192
|
+
}
|
|
193
|
+
return { ...row, readAt: stamp() };
|
|
194
|
+
});
|
|
195
|
+
if (localSeen && remote.unseenCount === 0) localSeen = false;
|
|
196
|
+
state = {
|
|
197
|
+
...remote,
|
|
198
|
+
notifications,
|
|
199
|
+
unseenCount: localSeen ? 0 : remote.unseenCount,
|
|
200
|
+
unreadCount: countUnread(notifications)
|
|
201
|
+
};
|
|
202
|
+
emit();
|
|
203
|
+
}
|
|
204
|
+
function onTabMessage(event) {
|
|
205
|
+
const message = event.data;
|
|
206
|
+
if (!message || typeof message !== "object") return;
|
|
207
|
+
if (role === "leader") {
|
|
208
|
+
if (message.type === "hello") post2({ type: "state", state });
|
|
209
|
+
else if (message.type === "mutated") void refresh().catch(() => {
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (message.type === "state") applyRemoteState(message.state);
|
|
214
|
+
}
|
|
215
|
+
function schedule(delay) {
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
timer = setTimeout(tick, delay);
|
|
218
|
+
}
|
|
219
|
+
function nextDelay() {
|
|
220
|
+
if (consecutiveFailures > 0) {
|
|
221
|
+
return Math.min(pollIntervalMs * 2 ** consecutiveFailures, maxPollIntervalMs);
|
|
222
|
+
}
|
|
223
|
+
if (streamConnected) return safetyNetMs;
|
|
224
|
+
if (now() < activeUntil) return pollIntervalMs;
|
|
225
|
+
idleTicks += 1;
|
|
226
|
+
return Math.min(pollIntervalMs * 2 ** idleTicks, maxPollIntervalMs);
|
|
227
|
+
}
|
|
228
|
+
async function tick() {
|
|
229
|
+
if (!running) return;
|
|
230
|
+
if (isHidden()) {
|
|
231
|
+
schedule(pollIntervalMs);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
await refresh();
|
|
236
|
+
consecutiveFailures = 0;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
consecutiveFailures += 1;
|
|
239
|
+
set({ isLoading: false, error: error instanceof Error ? error : new Error(String(error)) });
|
|
240
|
+
}
|
|
241
|
+
if (running) schedule(nextDelay());
|
|
242
|
+
}
|
|
243
|
+
function onActivity() {
|
|
244
|
+
if (!running || role === "follower") return;
|
|
245
|
+
activeUntil = now() + activeWindowMs;
|
|
246
|
+
idleTicks = 0;
|
|
247
|
+
if (!isHidden() && now() - lastRefreshAt > pollIntervalMs) {
|
|
248
|
+
void refresh().catch(() => {
|
|
249
|
+
});
|
|
250
|
+
schedule(nextDelay());
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const onVisibility = () => {
|
|
254
|
+
if (!isHidden()) onActivity();
|
|
255
|
+
};
|
|
256
|
+
const ACTIVITY_EVENTS = ["focus", "pointerdown", "keydown", "online"];
|
|
257
|
+
function changed() {
|
|
258
|
+
if (!running) return;
|
|
259
|
+
void refresh().catch(() => {
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async function readStream(signal) {
|
|
263
|
+
const openedAt = now();
|
|
264
|
+
const response = await doFetch(`${baseUrl}/events`, {
|
|
265
|
+
headers: { accept: "text/event-stream" },
|
|
266
|
+
signal
|
|
267
|
+
});
|
|
268
|
+
if (!response.ok) throw new Error(`GET /events failed: ${response.status}`);
|
|
269
|
+
if (!response.body) {
|
|
270
|
+
streamDisabled = true;
|
|
271
|
+
schedule(nextDelay());
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const reader = response.body.getReader();
|
|
275
|
+
const decoder = new TextDecoder();
|
|
276
|
+
let buffer = "";
|
|
277
|
+
let eventName = "";
|
|
278
|
+
try {
|
|
279
|
+
for (; ; ) {
|
|
280
|
+
const { done, value } = await reader.read();
|
|
281
|
+
if (done) break;
|
|
282
|
+
buffer += decoder.decode(value, { stream: true });
|
|
283
|
+
let newline = buffer.indexOf("\n");
|
|
284
|
+
while (newline >= 0) {
|
|
285
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
286
|
+
buffer = buffer.slice(newline + 1);
|
|
287
|
+
if (line === "") {
|
|
288
|
+
if (eventName === "ready") {
|
|
289
|
+
streamConnected = true;
|
|
290
|
+
streamFailures = 0;
|
|
291
|
+
schedule(nextDelay());
|
|
292
|
+
} else if (eventName === "changed") changed();
|
|
293
|
+
eventName = "";
|
|
294
|
+
} else if (line.startsWith("event:")) eventName = line.slice(6).trim();
|
|
295
|
+
newline = buffer.indexOf("\n");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} finally {
|
|
299
|
+
streamConnected = false;
|
|
300
|
+
if (now() - openedAt < SHORT_STREAM_MS) shortStreams += 1;
|
|
301
|
+
else shortStreams = 0;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const isFollower = () => role === "follower";
|
|
305
|
+
async function streamLoop() {
|
|
306
|
+
while (running && !streamDisabled && !isFollower()) {
|
|
307
|
+
streamAbort = new AbortController();
|
|
308
|
+
try {
|
|
309
|
+
await readStream(streamAbort.signal);
|
|
310
|
+
} catch {
|
|
311
|
+
if (!running) return;
|
|
312
|
+
streamFailures += 1;
|
|
313
|
+
}
|
|
314
|
+
if (!running || isFollower() || streamDisabled) return;
|
|
315
|
+
if (shortStreams >= SHORT_STREAMS_BEFORE_GIVING_UP) {
|
|
316
|
+
streamDisabled = true;
|
|
317
|
+
schedule(nextDelay());
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
await new Promise(
|
|
321
|
+
(resolve) => setTimeout(resolve, Math.min(3e3 * 2 ** streamFailures, 6e4))
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function becomeLeader() {
|
|
326
|
+
role = "leader";
|
|
327
|
+
post2({ type: "leader" });
|
|
328
|
+
post2({ type: "state", state });
|
|
329
|
+
void tick();
|
|
330
|
+
if (!streamDisabled) void streamLoop();
|
|
331
|
+
}
|
|
332
|
+
function becomeFollower() {
|
|
333
|
+
role = "follower";
|
|
334
|
+
post2({ type: "hello" });
|
|
335
|
+
}
|
|
336
|
+
function acquireLeadership() {
|
|
337
|
+
if (!locks || !channel) {
|
|
338
|
+
role = "solo";
|
|
339
|
+
void tick();
|
|
340
|
+
if (!streamDisabled) void streamLoop();
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
becomeFollower();
|
|
344
|
+
lockAbort = new AbortController();
|
|
345
|
+
locks.request(
|
|
346
|
+
`easy-ping:leader:${groupKey}`,
|
|
347
|
+
{ mode: "exclusive", signal: lockAbort.signal },
|
|
348
|
+
() => new Promise((resolve) => {
|
|
349
|
+
releaseLock = resolve;
|
|
350
|
+
if (running) becomeLeader();
|
|
351
|
+
else resolve();
|
|
352
|
+
})
|
|
353
|
+
).catch(() => {
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function start() {
|
|
357
|
+
running = true;
|
|
358
|
+
channel = makeChannel ? makeChannel() : void 0;
|
|
359
|
+
channel?.addEventListener("message", onTabMessage);
|
|
360
|
+
serviceWorker?.addEventListener("message", onRelay);
|
|
361
|
+
if (activityTarget) {
|
|
362
|
+
for (const name of ACTIVITY_EVENTS) activityTarget.addEventListener(name, onActivity);
|
|
363
|
+
}
|
|
364
|
+
if (typeof document !== "undefined") {
|
|
365
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
366
|
+
}
|
|
367
|
+
activeUntil = now() + activeWindowMs;
|
|
368
|
+
acquireLeadership();
|
|
369
|
+
}
|
|
370
|
+
function stop() {
|
|
371
|
+
running = false;
|
|
372
|
+
clearTimeout(timer);
|
|
373
|
+
timer = void 0;
|
|
374
|
+
streamAbort?.abort();
|
|
375
|
+
streamAbort = void 0;
|
|
376
|
+
lockAbort?.abort();
|
|
377
|
+
lockAbort = void 0;
|
|
378
|
+
releaseLock?.();
|
|
379
|
+
releaseLock = void 0;
|
|
380
|
+
channel?.removeEventListener("message", onTabMessage);
|
|
381
|
+
channel?.close();
|
|
382
|
+
channel = void 0;
|
|
383
|
+
serviceWorker?.removeEventListener("message", onRelay);
|
|
384
|
+
if (activityTarget) {
|
|
385
|
+
for (const name of ACTIVITY_EVENTS) activityTarget.removeEventListener(name, onActivity);
|
|
386
|
+
}
|
|
387
|
+
if (typeof document !== "undefined") {
|
|
388
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
389
|
+
}
|
|
390
|
+
role = "solo";
|
|
391
|
+
}
|
|
392
|
+
function onRelay(event) {
|
|
393
|
+
const data = event.data;
|
|
394
|
+
if (data?.source === "easy-ping" && data.type === "changed") changed();
|
|
395
|
+
}
|
|
396
|
+
let lastInboxVersion = null;
|
|
397
|
+
const sameOrigin = (response) => {
|
|
398
|
+
if (!response.url || typeof location === "undefined") return true;
|
|
399
|
+
try {
|
|
400
|
+
return new URL(response.url).origin === location.origin;
|
|
401
|
+
} catch {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
function instrument(inner) {
|
|
406
|
+
return (async (...args) => {
|
|
407
|
+
const response = await inner(...args);
|
|
408
|
+
const version = sameOrigin(response) ? response.headers.get("x-easy-ping-inbox") : null;
|
|
409
|
+
if (version !== null && version !== lastInboxVersion) {
|
|
410
|
+
const first = lastInboxVersion === null;
|
|
411
|
+
lastInboxVersion = version;
|
|
412
|
+
if (!first && role !== "follower") changed();
|
|
413
|
+
}
|
|
414
|
+
return response;
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
function subscribe(listener) {
|
|
418
|
+
listeners.add(listener);
|
|
419
|
+
listener(state);
|
|
420
|
+
if (listeners.size === 1) start();
|
|
421
|
+
return () => {
|
|
422
|
+
listeners.delete(listener);
|
|
423
|
+
if (listeners.size === 0) stop();
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
subscribe,
|
|
428
|
+
refresh,
|
|
429
|
+
loadMore,
|
|
430
|
+
markAsRead,
|
|
431
|
+
markAllRead,
|
|
432
|
+
markSeen,
|
|
433
|
+
instrument,
|
|
434
|
+
getState: () => state,
|
|
435
|
+
getTransport: () => ({
|
|
436
|
+
role,
|
|
437
|
+
transport: streamDisabled ? "poll" : "sse",
|
|
438
|
+
connected: streamConnected
|
|
439
|
+
})
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/react/index.ts
|
|
444
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
445
|
+
function useNotifications(options = {}) {
|
|
446
|
+
const {
|
|
447
|
+
client: provided,
|
|
448
|
+
baseUrl,
|
|
449
|
+
limit,
|
|
450
|
+
pollIntervalMs,
|
|
451
|
+
maxPollIntervalMs,
|
|
452
|
+
safetyNetMs,
|
|
453
|
+
activeWindowMs,
|
|
454
|
+
transport
|
|
455
|
+
} = options;
|
|
456
|
+
const callbacksRef = useRef(options);
|
|
457
|
+
callbacksRef.current = options;
|
|
458
|
+
const client = useMemo(
|
|
459
|
+
() => provided ?? createNotifyClient({
|
|
460
|
+
baseUrl,
|
|
461
|
+
limit,
|
|
462
|
+
pollIntervalMs,
|
|
463
|
+
maxPollIntervalMs,
|
|
464
|
+
safetyNetMs,
|
|
465
|
+
activeWindowMs,
|
|
466
|
+
transport,
|
|
467
|
+
fetch: (...args) => (callbacksRef.current.fetch ?? globalThis.fetch)(...args),
|
|
468
|
+
isDocumentHidden: () => callbacksRef.current.isDocumentHidden?.() ?? (typeof document !== "undefined" && document.visibilityState === "hidden")
|
|
469
|
+
}),
|
|
470
|
+
[
|
|
471
|
+
provided,
|
|
472
|
+
baseUrl,
|
|
473
|
+
limit,
|
|
474
|
+
pollIntervalMs,
|
|
475
|
+
maxPollIntervalMs,
|
|
476
|
+
safetyNetMs,
|
|
477
|
+
activeWindowMs,
|
|
478
|
+
transport
|
|
479
|
+
]
|
|
480
|
+
);
|
|
481
|
+
const [state, setState] = useState(() => client.getState());
|
|
482
|
+
useEffect(() => client.subscribe(setState), [client]);
|
|
483
|
+
return {
|
|
484
|
+
...state,
|
|
485
|
+
markAsRead: useCallback((id) => client.markAsRead(id), [client]),
|
|
486
|
+
markAllRead: useCallback(() => client.markAllRead(), [client]),
|
|
487
|
+
markSeen: useCallback(() => client.markSeen(), [client]),
|
|
488
|
+
loadMore: useCallback(() => client.loadMore(), [client]),
|
|
489
|
+
refresh: useCallback(() => client.refresh(), [client])
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/react-native/index.ts
|
|
494
|
+
function appStateTarget(appState) {
|
|
495
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
496
|
+
let subscription;
|
|
497
|
+
let last = appState.currentState;
|
|
498
|
+
const ensure = () => {
|
|
499
|
+
subscription ??= appState.addEventListener("change", (state) => {
|
|
500
|
+
if (state === "active" && last !== "active") {
|
|
501
|
+
for (const listener of listeners.get("focus") ?? []) listener({});
|
|
502
|
+
}
|
|
503
|
+
last = state;
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
return {
|
|
507
|
+
addEventListener(type, handler) {
|
|
508
|
+
listeners.set(type, (listeners.get(type) ?? /* @__PURE__ */ new Set()).add(handler));
|
|
509
|
+
ensure();
|
|
510
|
+
},
|
|
511
|
+
removeEventListener(type, handler) {
|
|
512
|
+
listeners.get(type)?.delete(handler);
|
|
513
|
+
if ([...listeners.values()].every((set) => set.size === 0)) {
|
|
514
|
+
subscription?.remove();
|
|
515
|
+
subscription = void 0;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function createNativeNotifyClient(options = {}) {
|
|
521
|
+
const { appState = AppState, ...rest } = options;
|
|
522
|
+
return createNotifyClient({
|
|
523
|
+
transport: "sse",
|
|
524
|
+
...rest,
|
|
525
|
+
locks: null,
|
|
526
|
+
channel: null,
|
|
527
|
+
serviceWorker: null,
|
|
528
|
+
isDocumentHidden: () => appState.currentState !== "active",
|
|
529
|
+
activityTarget: appStateTarget(appState)
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
function useNotifications2(options = {}) {
|
|
533
|
+
const {
|
|
534
|
+
client: provided,
|
|
535
|
+
baseUrl,
|
|
536
|
+
limit,
|
|
537
|
+
pollIntervalMs,
|
|
538
|
+
maxPollIntervalMs,
|
|
539
|
+
safetyNetMs,
|
|
540
|
+
activeWindowMs,
|
|
541
|
+
transport,
|
|
542
|
+
scope,
|
|
543
|
+
appState
|
|
544
|
+
} = options;
|
|
545
|
+
const fetchRef = useRef2(options.fetch);
|
|
546
|
+
fetchRef.current = options.fetch;
|
|
547
|
+
const client = useMemo2(
|
|
548
|
+
() => provided ?? createNativeNotifyClient({
|
|
549
|
+
baseUrl,
|
|
550
|
+
limit,
|
|
551
|
+
pollIntervalMs,
|
|
552
|
+
maxPollIntervalMs,
|
|
553
|
+
safetyNetMs,
|
|
554
|
+
activeWindowMs,
|
|
555
|
+
transport,
|
|
556
|
+
scope,
|
|
557
|
+
appState,
|
|
558
|
+
fetch: (...args) => (fetchRef.current ?? globalThis.fetch)(...args)
|
|
559
|
+
}),
|
|
560
|
+
[
|
|
561
|
+
provided,
|
|
562
|
+
baseUrl,
|
|
563
|
+
limit,
|
|
564
|
+
pollIntervalMs,
|
|
565
|
+
maxPollIntervalMs,
|
|
566
|
+
safetyNetMs,
|
|
567
|
+
activeWindowMs,
|
|
568
|
+
transport,
|
|
569
|
+
scope,
|
|
570
|
+
appState
|
|
571
|
+
]
|
|
572
|
+
);
|
|
573
|
+
return useNotifications({ client });
|
|
574
|
+
}
|
|
575
|
+
var post = async (doFetch, url, body) => doFetch(url, {
|
|
576
|
+
method: "POST",
|
|
577
|
+
headers: { "content-type": "application/json" },
|
|
578
|
+
body: JSON.stringify(body)
|
|
579
|
+
});
|
|
580
|
+
async function registerMobilePushDevice(options) {
|
|
581
|
+
const base = (options.baseUrl ?? "/api/notifications").replace(/\/$/, "");
|
|
582
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
583
|
+
const response = await post(doFetch, `${base}/mobile-push/devices`, {
|
|
584
|
+
token: options.token,
|
|
585
|
+
platform: options.platform,
|
|
586
|
+
deviceName: options.deviceName
|
|
587
|
+
});
|
|
588
|
+
if (!response.ok) {
|
|
589
|
+
const detail = await response.json().catch(() => ({}));
|
|
590
|
+
throw new Error(detail.error ?? `registering the device failed: ${response.status}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async function unregisterMobilePushDevice(options) {
|
|
594
|
+
const base = (options.baseUrl ?? "/api/notifications").replace(/\/$/, "");
|
|
595
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
596
|
+
const response = await post(doFetch, `${base}/mobile-push/devices/remove`, {
|
|
597
|
+
token: options.token
|
|
598
|
+
});
|
|
599
|
+
if (response.status === 404) return false;
|
|
600
|
+
if (!response.ok) throw new Error(`unregistering the device failed: ${response.status}`);
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
export {
|
|
604
|
+
createNativeNotifyClient,
|
|
605
|
+
registerMobilePushDevice,
|
|
606
|
+
unregisterMobilePushDevice,
|
|
607
|
+
useNotifications2 as useNotifications
|
|
608
|
+
};
|
|
609
|
+
//# sourceMappingURL=react-native.js.map
|