@emmanuel-nike/ark-notify-js 0.1.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/LICENSE +21 -0
- package/README.md +295 -0
- package/dist/index.cjs +564 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +552 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +963 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +109 -0
- package/dist/react/index.d.ts +109 -0
- package/dist/react/index.js +951 -0
- package/dist/react/index.js.map +1 -0
- package/dist/utils-Cw2SnD6p.d.cts +407 -0
- package/dist/utils-Cw2SnD6p.d.ts +407 -0
- package/package.json +120 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
import { createContext, useMemo, useRef, useEffect, useCallback, useState, useSyncExternalStore, useContext } from 'react';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/config.ts
|
|
5
|
+
var DEFAULT_BASE_URL = "https://ark-notify-933303906015.europe-north1.run.app";
|
|
6
|
+
var configuredBaseUrl;
|
|
7
|
+
function configureArkNotify(config) {
|
|
8
|
+
if (config.baseUrl !== void 0) {
|
|
9
|
+
configuredBaseUrl = config.baseUrl;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function resolveBaseUrl(baseUrl) {
|
|
13
|
+
return (baseUrl ?? configuredBaseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/utils.ts
|
|
17
|
+
var ArkNotifyError = class extends Error {
|
|
18
|
+
constructor(status, body) {
|
|
19
|
+
super(body.message);
|
|
20
|
+
this.name = "ArkNotifyError";
|
|
21
|
+
this.status = status;
|
|
22
|
+
this.code = body.error;
|
|
23
|
+
this.retryAfterSec = body.retryAfterSec;
|
|
24
|
+
this.reason = body.reason;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
function toWebSocketUrl(baseUrl, path) {
|
|
28
|
+
const url = new URL(path, baseUrl);
|
|
29
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
30
|
+
return url.toString();
|
|
31
|
+
}
|
|
32
|
+
function resolveValue(value) {
|
|
33
|
+
return typeof value === "function" ? value() : value;
|
|
34
|
+
}
|
|
35
|
+
function isPrivateChannel(channel) {
|
|
36
|
+
return channel.startsWith("private-");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/client.ts
|
|
40
|
+
var ArkNotifyClient = class {
|
|
41
|
+
constructor(config) {
|
|
42
|
+
this.baseUrl = resolveBaseUrl(config.baseUrl);
|
|
43
|
+
this.token = config.token;
|
|
44
|
+
this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
45
|
+
}
|
|
46
|
+
setToken(token) {
|
|
47
|
+
this.token = token ?? void 0;
|
|
48
|
+
}
|
|
49
|
+
getAuthHeader() {
|
|
50
|
+
const token = resolveValue(this.token);
|
|
51
|
+
return token ? `Bearer ${token}` : void 0;
|
|
52
|
+
}
|
|
53
|
+
async request(path, options = {}) {
|
|
54
|
+
const headers = {
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
...options.headers
|
|
57
|
+
};
|
|
58
|
+
const auth = this.getAuthHeader();
|
|
59
|
+
if (auth) {
|
|
60
|
+
headers.Authorization = auth;
|
|
61
|
+
}
|
|
62
|
+
if (options.credentials) {
|
|
63
|
+
headers["X-App-Key"] = options.credentials.appKey;
|
|
64
|
+
headers["X-App-Secret"] = options.credentials.secret;
|
|
65
|
+
}
|
|
66
|
+
const response = await this.fetchFn(`${this.baseUrl}${path}`, {
|
|
67
|
+
method: options.method ?? "GET",
|
|
68
|
+
headers,
|
|
69
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
70
|
+
});
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
let body;
|
|
73
|
+
try {
|
|
74
|
+
body = await response.json();
|
|
75
|
+
} catch {
|
|
76
|
+
body = { error: "request_failed", message: response.statusText };
|
|
77
|
+
}
|
|
78
|
+
throw new ArkNotifyError(response.status, body);
|
|
79
|
+
}
|
|
80
|
+
if (response.status === 204) {
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
return response.json();
|
|
84
|
+
}
|
|
85
|
+
// ── Health ──────────────────────────────────────────────────────────────
|
|
86
|
+
health() {
|
|
87
|
+
return this.request("/health");
|
|
88
|
+
}
|
|
89
|
+
// ── Platform auth ───────────────────────────────────────────────────────
|
|
90
|
+
login(input) {
|
|
91
|
+
return this.request("/api/v1/auth/login", { method: "POST", body: input });
|
|
92
|
+
}
|
|
93
|
+
me() {
|
|
94
|
+
return this.request("/api/v1/auth/me");
|
|
95
|
+
}
|
|
96
|
+
// ── Applications ────────────────────────────────────────────────────────
|
|
97
|
+
listApplications() {
|
|
98
|
+
return this.request("/api/v1/applications");
|
|
99
|
+
}
|
|
100
|
+
createApplication(input) {
|
|
101
|
+
return this.request("/api/v1/applications", { method: "POST", body: input });
|
|
102
|
+
}
|
|
103
|
+
getApplication(id) {
|
|
104
|
+
return this.request(`/api/v1/applications/${id}`);
|
|
105
|
+
}
|
|
106
|
+
updateApplication(id, input) {
|
|
107
|
+
return this.request(`/api/v1/applications/${id}`, { method: "PUT", body: input });
|
|
108
|
+
}
|
|
109
|
+
deleteApplication(id) {
|
|
110
|
+
return this.request(`/api/v1/applications/${id}`, { method: "DELETE" });
|
|
111
|
+
}
|
|
112
|
+
regenerateSecret(id) {
|
|
113
|
+
return this.request(`/api/v1/applications/${id}/regenerate-secret`, {
|
|
114
|
+
method: "POST"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// ── System admin ────────────────────────────────────────────────────────
|
|
118
|
+
adminChannels() {
|
|
119
|
+
return this.request("/api/v1/admin/channels");
|
|
120
|
+
}
|
|
121
|
+
// ── Data plane ──────────────────────────────────────────────────────────
|
|
122
|
+
publishEvent(appKey, credentials, input) {
|
|
123
|
+
return this.request(`/api/v1/apps/${appKey}/events`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
body: input,
|
|
126
|
+
credentials
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
authorizeChannel(appKey, credentials, input) {
|
|
130
|
+
return this.request(`/api/v1/apps/${appKey}/auth`, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
body: input,
|
|
133
|
+
credentials
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
issueConnectionToken(appKey, credentials, input) {
|
|
137
|
+
return this.request(`/api/v1/apps/${appKey}/connection-token`, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
body: input,
|
|
140
|
+
credentials
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// src/connection-token.ts
|
|
146
|
+
async function fetchConnectionToken(options) {
|
|
147
|
+
const {
|
|
148
|
+
baseUrl,
|
|
149
|
+
appKey,
|
|
150
|
+
credentials,
|
|
151
|
+
fetch: fetchFn = globalThis.fetch.bind(globalThis),
|
|
152
|
+
client_id,
|
|
153
|
+
clientId,
|
|
154
|
+
user_data,
|
|
155
|
+
userData,
|
|
156
|
+
ttl,
|
|
157
|
+
capabilities,
|
|
158
|
+
serverAuthUrl
|
|
159
|
+
} = options;
|
|
160
|
+
const resolvedClientId = client_id ?? clientId;
|
|
161
|
+
if (!resolvedClientId) {
|
|
162
|
+
throw new Error("client_id is required to fetch a connection token");
|
|
163
|
+
}
|
|
164
|
+
const body = {
|
|
165
|
+
client_id: resolvedClientId,
|
|
166
|
+
user_data: user_data ?? userData,
|
|
167
|
+
ttl,
|
|
168
|
+
capabilities,
|
|
169
|
+
serverAuthUrl
|
|
170
|
+
};
|
|
171
|
+
const url = `${resolveBaseUrl(baseUrl)}/api/v1/apps/${appKey}/connection-token`;
|
|
172
|
+
const response = await fetchFn(url, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: {
|
|
175
|
+
"Content-Type": "application/json",
|
|
176
|
+
"X-App-Key": credentials.appKey,
|
|
177
|
+
"X-App-Secret": credentials.secret
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify(body)
|
|
180
|
+
});
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
let errorBody;
|
|
183
|
+
try {
|
|
184
|
+
errorBody = await response.json();
|
|
185
|
+
} catch {
|
|
186
|
+
errorBody = { error: "request_failed", message: response.statusText };
|
|
187
|
+
}
|
|
188
|
+
throw new ArkNotifyError(response.status, errorBody);
|
|
189
|
+
}
|
|
190
|
+
return response.json();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/connection.ts
|
|
194
|
+
var ArkNotifyConnection = class {
|
|
195
|
+
constructor(config) {
|
|
196
|
+
this.ws = null;
|
|
197
|
+
this.state = "disconnected";
|
|
198
|
+
this.connectionId = null;
|
|
199
|
+
this.clientId = null;
|
|
200
|
+
this.authenticated = false;
|
|
201
|
+
this.subscribedChannels = /* @__PURE__ */ new Set();
|
|
202
|
+
this.pendingSubscriptions = /* @__PURE__ */ new Map();
|
|
203
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
204
|
+
this.reconnectAttempt = 0;
|
|
205
|
+
this.reconnectTimer = null;
|
|
206
|
+
this.intentionalClose = false;
|
|
207
|
+
this.connectPromise = null;
|
|
208
|
+
this.config = {
|
|
209
|
+
autoReconnect: true,
|
|
210
|
+
reconnectDelayMs: 1e3,
|
|
211
|
+
maxReconnectDelayMs: 3e4,
|
|
212
|
+
...config,
|
|
213
|
+
baseUrl: resolveBaseUrl(config.baseUrl)
|
|
214
|
+
};
|
|
215
|
+
this.WebSocketCtor = config.WebSocket ?? globalThis.WebSocket;
|
|
216
|
+
}
|
|
217
|
+
getConnectionState() {
|
|
218
|
+
return this.state;
|
|
219
|
+
}
|
|
220
|
+
getConnectionId() {
|
|
221
|
+
return this.connectionId;
|
|
222
|
+
}
|
|
223
|
+
getClientId() {
|
|
224
|
+
return this.clientId;
|
|
225
|
+
}
|
|
226
|
+
isAuthenticated() {
|
|
227
|
+
return this.authenticated;
|
|
228
|
+
}
|
|
229
|
+
getSubscribedChannels() {
|
|
230
|
+
return [...this.subscribedChannels];
|
|
231
|
+
}
|
|
232
|
+
on(event, handler) {
|
|
233
|
+
if (!this.listeners.has(event)) {
|
|
234
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
235
|
+
}
|
|
236
|
+
this.listeners.get(event).add(handler);
|
|
237
|
+
return () => this.off(event, handler);
|
|
238
|
+
}
|
|
239
|
+
off(event, handler) {
|
|
240
|
+
this.listeners.get(event)?.delete(handler);
|
|
241
|
+
}
|
|
242
|
+
emit(event, ...args) {
|
|
243
|
+
for (const handler of this.listeners.get(event) ?? []) {
|
|
244
|
+
handler(...args);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
setState(state) {
|
|
248
|
+
if (this.state !== state) {
|
|
249
|
+
this.state = state;
|
|
250
|
+
this.emit("state", state);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
connect() {
|
|
254
|
+
if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING) {
|
|
255
|
+
return Promise.resolve();
|
|
256
|
+
}
|
|
257
|
+
if (this.connectPromise) {
|
|
258
|
+
return this.connectPromise;
|
|
259
|
+
}
|
|
260
|
+
this.connectPromise = this.doConnect().finally(() => {
|
|
261
|
+
this.connectPromise = null;
|
|
262
|
+
});
|
|
263
|
+
return this.connectPromise;
|
|
264
|
+
}
|
|
265
|
+
async doConnect() {
|
|
266
|
+
this.intentionalClose = false;
|
|
267
|
+
this.clearReconnectTimer();
|
|
268
|
+
this.setState(this.reconnectAttempt > 0 ? "reconnecting" : "connecting");
|
|
269
|
+
const url = new URL(
|
|
270
|
+
toWebSocketUrl(this.config.baseUrl, `/app/${this.config.appKey}`)
|
|
271
|
+
);
|
|
272
|
+
let token = resolveValue(this.config.token);
|
|
273
|
+
if (!token && this.config.clientId && this.config.credentials) {
|
|
274
|
+
const result = await fetchConnectionToken({
|
|
275
|
+
baseUrl: this.config.baseUrl,
|
|
276
|
+
appKey: this.config.appKey,
|
|
277
|
+
credentials: this.config.credentials,
|
|
278
|
+
client_id: this.config.clientId,
|
|
279
|
+
user_data: this.config.user_data,
|
|
280
|
+
fetch: this.config.fetch
|
|
281
|
+
});
|
|
282
|
+
token = result.token;
|
|
283
|
+
}
|
|
284
|
+
if (token) {
|
|
285
|
+
url.searchParams.set("token", token);
|
|
286
|
+
} else if (this.config.clientId) {
|
|
287
|
+
url.searchParams.set("clientId", this.config.clientId);
|
|
288
|
+
}
|
|
289
|
+
this.ws = new this.WebSocketCtor(url.toString());
|
|
290
|
+
this.ws.onopen = () => {
|
|
291
|
+
this.reconnectAttempt = 0;
|
|
292
|
+
};
|
|
293
|
+
this.ws.onmessage = (event) => {
|
|
294
|
+
this.handleMessage(event.data);
|
|
295
|
+
};
|
|
296
|
+
this.ws.onerror = () => {
|
|
297
|
+
this.emit("error", { code: "websocket_error", message: "WebSocket error" });
|
|
298
|
+
};
|
|
299
|
+
this.ws.onclose = (event) => {
|
|
300
|
+
this.ws = null;
|
|
301
|
+
this.connectionId = null;
|
|
302
|
+
this.emit("close", { code: event.code, reason: event.reason });
|
|
303
|
+
if (!this.intentionalClose && this.config.autoReconnect) {
|
|
304
|
+
this.scheduleReconnect();
|
|
305
|
+
} else {
|
|
306
|
+
this.setState("failed");
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
disconnect() {
|
|
311
|
+
this.intentionalClose = true;
|
|
312
|
+
this.clearReconnectTimer();
|
|
313
|
+
this.subscribedChannels.clear();
|
|
314
|
+
this.pendingSubscriptions.clear();
|
|
315
|
+
if (this.ws) {
|
|
316
|
+
this.ws.close();
|
|
317
|
+
this.ws = null;
|
|
318
|
+
}
|
|
319
|
+
this.setState("disconnected");
|
|
320
|
+
}
|
|
321
|
+
scheduleReconnect() {
|
|
322
|
+
this.setState("reconnecting");
|
|
323
|
+
const delay = Math.min(
|
|
324
|
+
this.config.reconnectDelayMs * 2 ** this.reconnectAttempt,
|
|
325
|
+
this.config.maxReconnectDelayMs
|
|
326
|
+
);
|
|
327
|
+
this.reconnectAttempt++;
|
|
328
|
+
this.reconnectTimer = setTimeout(() => void this.connect(), delay);
|
|
329
|
+
}
|
|
330
|
+
clearReconnectTimer() {
|
|
331
|
+
if (this.reconnectTimer) {
|
|
332
|
+
clearTimeout(this.reconnectTimer);
|
|
333
|
+
this.reconnectTimer = null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
send(payload) {
|
|
337
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
338
|
+
throw new Error("WebSocket is not connected");
|
|
339
|
+
}
|
|
340
|
+
this.ws.send(JSON.stringify(payload));
|
|
341
|
+
}
|
|
342
|
+
handleMessage(raw) {
|
|
343
|
+
let message;
|
|
344
|
+
try {
|
|
345
|
+
message = JSON.parse(raw);
|
|
346
|
+
} catch {
|
|
347
|
+
this.emit("error", { code: "invalid_json", message: "Invalid JSON from server" });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
this.emit("message", message);
|
|
351
|
+
switch (message.type) {
|
|
352
|
+
case "connected": {
|
|
353
|
+
this.connectionId = message.connection_id;
|
|
354
|
+
this.clientId = message.client_id;
|
|
355
|
+
this.authenticated = message.authenticated;
|
|
356
|
+
this.setState("connected");
|
|
357
|
+
this.emit("connected", message);
|
|
358
|
+
this.resubscribeAll();
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
case "event":
|
|
362
|
+
this.emit("event", message);
|
|
363
|
+
break;
|
|
364
|
+
case "presence":
|
|
365
|
+
this.emit("presence", message);
|
|
366
|
+
break;
|
|
367
|
+
case "subscribed":
|
|
368
|
+
this.subscribedChannels.add(message.channel);
|
|
369
|
+
break;
|
|
370
|
+
case "unsubscribed":
|
|
371
|
+
this.subscribedChannels.delete(message.channel);
|
|
372
|
+
break;
|
|
373
|
+
case "error":
|
|
374
|
+
this.emit("error", { code: message.code, message: message.message });
|
|
375
|
+
break;
|
|
376
|
+
case "ping":
|
|
377
|
+
this.send({ action: "ping" });
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
resubscribeAll() {
|
|
382
|
+
for (const [channel, options] of this.pendingSubscriptions) {
|
|
383
|
+
void this.subscribe(channel, options);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async subscribe(channel, options = {}) {
|
|
387
|
+
this.pendingSubscriptions.set(channel, options);
|
|
388
|
+
if (this.state !== "connected" || !this.connectionId) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const payload = {
|
|
392
|
+
action: "subscribe",
|
|
393
|
+
channel
|
|
394
|
+
};
|
|
395
|
+
if (options.history) payload.history = true;
|
|
396
|
+
if (options.presence) {
|
|
397
|
+
payload.presence = true;
|
|
398
|
+
if (options.presence_data) payload.presence_data = options.presence_data;
|
|
399
|
+
}
|
|
400
|
+
if (isPrivateChannel(channel)) {
|
|
401
|
+
if (options.auth) {
|
|
402
|
+
payload.auth = options.auth;
|
|
403
|
+
} else if (this.config.onPrivateChannelAuth) {
|
|
404
|
+
payload.auth = await this.config.onPrivateChannelAuth(channel, this.connectionId);
|
|
405
|
+
} else {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`Private channel "${channel}" requires auth. Provide options.auth or onPrivateChannelAuth.`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
this.send(payload);
|
|
412
|
+
}
|
|
413
|
+
unsubscribe(channel) {
|
|
414
|
+
this.pendingSubscriptions.delete(channel);
|
|
415
|
+
this.subscribedChannels.delete(channel);
|
|
416
|
+
if (this.state === "connected") {
|
|
417
|
+
this.send({ action: "unsubscribe", channel });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
publish(channel, event, data) {
|
|
421
|
+
this.send({ action: "publish", channel, event, data });
|
|
422
|
+
}
|
|
423
|
+
presenceEnter(channel, data) {
|
|
424
|
+
this.send({ action: "presence_enter", channel, data });
|
|
425
|
+
}
|
|
426
|
+
presenceUpdate(channel, data) {
|
|
427
|
+
this.send({ action: "presence_update", channel, data });
|
|
428
|
+
}
|
|
429
|
+
presenceLeave(channel) {
|
|
430
|
+
this.send({ action: "presence_leave", channel });
|
|
431
|
+
}
|
|
432
|
+
presenceSync(channel) {
|
|
433
|
+
this.send({ action: "presence_sync", channel });
|
|
434
|
+
}
|
|
435
|
+
ping() {
|
|
436
|
+
this.send({ action: "ping" });
|
|
437
|
+
}
|
|
438
|
+
bind(channel, event, handler) {
|
|
439
|
+
const listener = (message) => {
|
|
440
|
+
if (message.channel === channel && message.event === event) {
|
|
441
|
+
handler(message.data, message);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
return this.on("event", listener);
|
|
445
|
+
}
|
|
446
|
+
bindAll(channel, handler) {
|
|
447
|
+
const listener = (message) => {
|
|
448
|
+
if (message.channel === channel) {
|
|
449
|
+
handler(message.data, message);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
return this.on("event", listener);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// src/sse.ts
|
|
457
|
+
var ArkNotifySSE = class {
|
|
458
|
+
constructor(config) {
|
|
459
|
+
this.es = null;
|
|
460
|
+
this.connectionId = null;
|
|
461
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
462
|
+
this.config = { ...config, baseUrl: resolveBaseUrl(config.baseUrl) };
|
|
463
|
+
this.EventSourceCtor = config.EventSource ?? globalThis.EventSource;
|
|
464
|
+
}
|
|
465
|
+
getConnectionId() {
|
|
466
|
+
return this.connectionId;
|
|
467
|
+
}
|
|
468
|
+
on(event, handler) {
|
|
469
|
+
if (!this.listeners.has(event)) {
|
|
470
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
471
|
+
}
|
|
472
|
+
this.listeners.get(event).add(handler);
|
|
473
|
+
return () => this.off(event, handler);
|
|
474
|
+
}
|
|
475
|
+
off(event, handler) {
|
|
476
|
+
this.listeners.get(event)?.delete(handler);
|
|
477
|
+
}
|
|
478
|
+
emit(event, ...args) {
|
|
479
|
+
for (const handler of this.listeners.get(event) ?? []) {
|
|
480
|
+
handler(...args);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async connect() {
|
|
484
|
+
if (this.es) return;
|
|
485
|
+
const base = this.config.baseUrl.replace(/\/$/, "");
|
|
486
|
+
const url = new URL(`${base}/app/${this.config.appKey}/stream`);
|
|
487
|
+
url.searchParams.set("channels", this.config.channels.join(","));
|
|
488
|
+
const token = resolveValue(this.config.token);
|
|
489
|
+
if (token) {
|
|
490
|
+
url.searchParams.set("token", token);
|
|
491
|
+
} else if (this.config.clientId) {
|
|
492
|
+
url.searchParams.set("clientId", this.config.clientId);
|
|
493
|
+
}
|
|
494
|
+
if (this.config.history) {
|
|
495
|
+
url.searchParams.set("history", "true");
|
|
496
|
+
}
|
|
497
|
+
if (this.config.user_data) {
|
|
498
|
+
url.searchParams.set("user_data", JSON.stringify(this.config.user_data));
|
|
499
|
+
}
|
|
500
|
+
let authMap = this.config.auth ? { ...this.config.auth } : {};
|
|
501
|
+
const privateChannels = this.config.channels.filter(isPrivateChannel);
|
|
502
|
+
if (privateChannels.length > 0 && this.config.onPrivateChannelAuth && !this.config.auth) ;
|
|
503
|
+
if (Object.keys(authMap).length > 0) {
|
|
504
|
+
url.searchParams.set("auth", JSON.stringify(authMap));
|
|
505
|
+
}
|
|
506
|
+
this.es = new this.EventSourceCtor(url.toString());
|
|
507
|
+
this.es.addEventListener("connected", (e) => {
|
|
508
|
+
const message = JSON.parse(e.data);
|
|
509
|
+
this.connectionId = message.connection_id;
|
|
510
|
+
this.emit("connected", message);
|
|
511
|
+
this.emit("message", message);
|
|
512
|
+
});
|
|
513
|
+
this.es.addEventListener("event", (e) => {
|
|
514
|
+
const message = JSON.parse(e.data);
|
|
515
|
+
this.emit("event", message);
|
|
516
|
+
this.emit("message", message);
|
|
517
|
+
});
|
|
518
|
+
this.es.addEventListener("presence", (e) => {
|
|
519
|
+
const message = JSON.parse(e.data);
|
|
520
|
+
this.emit("presence", message);
|
|
521
|
+
this.emit("message", message);
|
|
522
|
+
});
|
|
523
|
+
this.es.onerror = () => {
|
|
524
|
+
this.emit("error", new Error("SSE connection error"));
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
disconnect() {
|
|
528
|
+
if (this.es) {
|
|
529
|
+
this.es.close();
|
|
530
|
+
this.es = null;
|
|
531
|
+
this.connectionId = null;
|
|
532
|
+
this.emit("close");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
bind(channel, event, handler) {
|
|
536
|
+
const listener = (message) => {
|
|
537
|
+
if (message.channel === channel && message.event === event) {
|
|
538
|
+
handler(message.data, message);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
return this.on("event", listener);
|
|
542
|
+
}
|
|
543
|
+
bindAll(channel, handler) {
|
|
544
|
+
const listener = (message) => {
|
|
545
|
+
if (message.channel === channel) {
|
|
546
|
+
handler(message.data, message);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
return this.on("event", listener);
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
var ArkNotifyContext = createContext(null);
|
|
553
|
+
function useArkNotifyContext() {
|
|
554
|
+
const ctx = useContext(ArkNotifyContext);
|
|
555
|
+
if (!ctx) {
|
|
556
|
+
throw new Error("useArkNotify must be used within an ArkNotifyProvider");
|
|
557
|
+
}
|
|
558
|
+
return ctx;
|
|
559
|
+
}
|
|
560
|
+
function ArkNotifyProvider({
|
|
561
|
+
children,
|
|
562
|
+
baseUrl,
|
|
563
|
+
token,
|
|
564
|
+
fetch: fetchFn
|
|
565
|
+
}) {
|
|
566
|
+
const resolvedBaseUrl = useMemo(() => resolveBaseUrl(baseUrl), [baseUrl]);
|
|
567
|
+
const clientRef = useRef(null);
|
|
568
|
+
clientRef.current ?? (clientRef.current = new ArkNotifyClient({ baseUrl: resolvedBaseUrl, token, fetch: fetchFn }));
|
|
569
|
+
const client = clientRef.current;
|
|
570
|
+
useEffect(() => {
|
|
571
|
+
if (token === void 0) return;
|
|
572
|
+
const resolved = typeof token === "function" ? token() ?? null : token ?? null;
|
|
573
|
+
client.setToken(resolved);
|
|
574
|
+
}, [client, token]);
|
|
575
|
+
const createConnection = useCallback(
|
|
576
|
+
(config) => new ArkNotifyConnection({ baseUrl: resolvedBaseUrl, ...config }),
|
|
577
|
+
[resolvedBaseUrl]
|
|
578
|
+
);
|
|
579
|
+
const createSSE = useCallback(
|
|
580
|
+
(config) => new ArkNotifySSE({ baseUrl: resolvedBaseUrl, ...config }),
|
|
581
|
+
[resolvedBaseUrl]
|
|
582
|
+
);
|
|
583
|
+
const value = useMemo(
|
|
584
|
+
() => ({ baseUrl: resolvedBaseUrl, client, createConnection, createSSE }),
|
|
585
|
+
[resolvedBaseUrl, client, createConnection, createSSE]
|
|
586
|
+
);
|
|
587
|
+
return /* @__PURE__ */ jsx(ArkNotifyContext.Provider, { value, children });
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/react/useArkNotify.ts
|
|
591
|
+
function useArkNotify() {
|
|
592
|
+
return useArkNotifyContext();
|
|
593
|
+
}
|
|
594
|
+
function useConnection(options) {
|
|
595
|
+
const { createConnection } = useArkNotify();
|
|
596
|
+
const { enabled = true, ...connectionConfig } = options;
|
|
597
|
+
const connectionRef = useRef(null);
|
|
598
|
+
const [connectedMessage, setConnectedMessage] = useState(null);
|
|
599
|
+
if (!connectionRef.current) {
|
|
600
|
+
connectionRef.current = createConnection(connectionConfig);
|
|
601
|
+
}
|
|
602
|
+
const connection = connectionRef.current;
|
|
603
|
+
const subscribe = useCallback(
|
|
604
|
+
(onStoreChange) => {
|
|
605
|
+
const unsubscribers = [
|
|
606
|
+
connection.on("state", onStoreChange),
|
|
607
|
+
connection.on("connected", (msg) => {
|
|
608
|
+
setConnectedMessage(msg);
|
|
609
|
+
onStoreChange();
|
|
610
|
+
})
|
|
611
|
+
];
|
|
612
|
+
return () => unsubscribers.forEach((u) => u());
|
|
613
|
+
},
|
|
614
|
+
[connection]
|
|
615
|
+
);
|
|
616
|
+
const getSnapshot = useCallback(() => connection.getConnectionState(), [connection]);
|
|
617
|
+
const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
618
|
+
useEffect(() => {
|
|
619
|
+
if (enabled) {
|
|
620
|
+
connection.connect();
|
|
621
|
+
}
|
|
622
|
+
return () => connection.disconnect();
|
|
623
|
+
}, [connection, enabled]);
|
|
624
|
+
const connect = useCallback(() => connection.connect(), [connection]);
|
|
625
|
+
const disconnect = useCallback(() => connection.disconnect(), [connection]);
|
|
626
|
+
return {
|
|
627
|
+
connection,
|
|
628
|
+
state,
|
|
629
|
+
connectionId: connection.getConnectionId(),
|
|
630
|
+
clientId: connection.getClientId(),
|
|
631
|
+
authenticated: connection.isAuthenticated(),
|
|
632
|
+
connectedMessage,
|
|
633
|
+
connect,
|
|
634
|
+
disconnect
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function useChannel(connection, channel, options = {}) {
|
|
638
|
+
const { enabled = true, onEvent, ...subscribeOptions } = options;
|
|
639
|
+
const [subscribed, setSubscribed] = useState(false);
|
|
640
|
+
const subscribeOptionsRef = useRef(subscribeOptions);
|
|
641
|
+
subscribeOptionsRef.current = subscribeOptions;
|
|
642
|
+
const subscribe = useCallback(
|
|
643
|
+
async (opts) => {
|
|
644
|
+
if (!connection) return;
|
|
645
|
+
await connection.subscribe(channel, { ...subscribeOptionsRef.current, ...opts });
|
|
646
|
+
setSubscribed(true);
|
|
647
|
+
},
|
|
648
|
+
[connection, channel]
|
|
649
|
+
);
|
|
650
|
+
const unsubscribe = useCallback(() => {
|
|
651
|
+
connection?.unsubscribe(channel);
|
|
652
|
+
setSubscribed(false);
|
|
653
|
+
}, [connection, channel]);
|
|
654
|
+
const publish = useCallback(
|
|
655
|
+
(event, data) => {
|
|
656
|
+
connection?.publish(channel, event, data);
|
|
657
|
+
},
|
|
658
|
+
[connection, channel]
|
|
659
|
+
);
|
|
660
|
+
const bind = useCallback(
|
|
661
|
+
(event, handler) => {
|
|
662
|
+
if (!connection) return () => {
|
|
663
|
+
};
|
|
664
|
+
return connection.bind(channel, event, handler);
|
|
665
|
+
},
|
|
666
|
+
[connection, channel]
|
|
667
|
+
);
|
|
668
|
+
useEffect(() => {
|
|
669
|
+
if (!connection || !enabled || !channel) return;
|
|
670
|
+
const unsubSubscribed = connection.on("message", (msg) => {
|
|
671
|
+
if (msg.type === "subscribed" && msg.channel === channel) {
|
|
672
|
+
setSubscribed(true);
|
|
673
|
+
}
|
|
674
|
+
if (msg.type === "unsubscribed" && msg.channel === channel) {
|
|
675
|
+
setSubscribed(false);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
const unsubEvent = onEvent ? connection.on("event", (msg) => {
|
|
679
|
+
if (msg.channel === channel) {
|
|
680
|
+
onEvent(msg.event, msg.data, msg);
|
|
681
|
+
}
|
|
682
|
+
}) : () => {
|
|
683
|
+
};
|
|
684
|
+
void subscribe();
|
|
685
|
+
return () => {
|
|
686
|
+
unsubSubscribed();
|
|
687
|
+
unsubEvent();
|
|
688
|
+
connection.unsubscribe(channel);
|
|
689
|
+
setSubscribed(false);
|
|
690
|
+
};
|
|
691
|
+
}, [connection, channel, enabled, subscribe, onEvent]);
|
|
692
|
+
return { subscribed, subscribe, unsubscribe, publish, bind };
|
|
693
|
+
}
|
|
694
|
+
function usePresence(connection, channel, options = {}) {
|
|
695
|
+
const { enabled = true, initialData } = options;
|
|
696
|
+
const [members, setMembers] = useState([]);
|
|
697
|
+
useEffect(() => {
|
|
698
|
+
if (!connection || !enabled || !channel) return;
|
|
699
|
+
const unsubscribe = connection.on("presence", (msg) => {
|
|
700
|
+
if (msg.channel !== channel) return;
|
|
701
|
+
if (msg.action === "sync" && msg.members) {
|
|
702
|
+
setMembers(msg.members);
|
|
703
|
+
} else if (msg.action === "enter" && msg.member) {
|
|
704
|
+
setMembers((prev) => {
|
|
705
|
+
const filtered = prev.filter((m) => m.clientId !== msg.member.clientId);
|
|
706
|
+
return [...filtered, msg.member];
|
|
707
|
+
});
|
|
708
|
+
} else if (msg.action === "leave" && msg.member) {
|
|
709
|
+
setMembers((prev) => prev.filter((m) => m.clientId !== msg.member.clientId));
|
|
710
|
+
} else if (msg.action === "update" && msg.member) {
|
|
711
|
+
setMembers(
|
|
712
|
+
(prev) => prev.map((m) => m.clientId === msg.member.clientId ? msg.member : m)
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
if (initialData) {
|
|
717
|
+
connection.presenceEnter(channel, initialData);
|
|
718
|
+
}
|
|
719
|
+
return unsubscribe;
|
|
720
|
+
}, [connection, channel, enabled, initialData]);
|
|
721
|
+
const enter = useCallback(
|
|
722
|
+
(data) => connection?.presenceEnter(channel, data),
|
|
723
|
+
[connection, channel]
|
|
724
|
+
);
|
|
725
|
+
const update = useCallback(
|
|
726
|
+
(data) => connection?.presenceUpdate(channel, data),
|
|
727
|
+
[connection, channel]
|
|
728
|
+
);
|
|
729
|
+
const leave = useCallback(() => connection?.presenceLeave(channel), [connection, channel]);
|
|
730
|
+
const sync = useCallback(() => connection?.presenceSync(channel), [connection, channel]);
|
|
731
|
+
return { members, enter, update, leave, sync };
|
|
732
|
+
}
|
|
733
|
+
function useApplications() {
|
|
734
|
+
const { client } = useArkNotify();
|
|
735
|
+
const [apps, setApps] = useState([]);
|
|
736
|
+
const [loading, setLoading] = useState(true);
|
|
737
|
+
const [error, setError] = useState(null);
|
|
738
|
+
const refresh = useCallback(async () => {
|
|
739
|
+
setLoading(true);
|
|
740
|
+
setError(null);
|
|
741
|
+
try {
|
|
742
|
+
const { apps: list } = await client.listApplications();
|
|
743
|
+
setApps(list);
|
|
744
|
+
} catch (err) {
|
|
745
|
+
if (err instanceof ArkNotifyError) setError(err);
|
|
746
|
+
} finally {
|
|
747
|
+
setLoading(false);
|
|
748
|
+
}
|
|
749
|
+
}, [client]);
|
|
750
|
+
useEffect(() => {
|
|
751
|
+
void refresh();
|
|
752
|
+
}, [refresh]);
|
|
753
|
+
const create = useCallback(
|
|
754
|
+
async (input) => {
|
|
755
|
+
const { app } = await client.createApplication(input);
|
|
756
|
+
setApps((prev) => [...prev, app]);
|
|
757
|
+
return app;
|
|
758
|
+
},
|
|
759
|
+
[client]
|
|
760
|
+
);
|
|
761
|
+
const update = useCallback(
|
|
762
|
+
async (id, input) => {
|
|
763
|
+
const { app } = await client.updateApplication(id, input);
|
|
764
|
+
setApps((prev) => prev.map((a) => a.id === id ? app : a));
|
|
765
|
+
return app;
|
|
766
|
+
},
|
|
767
|
+
[client]
|
|
768
|
+
);
|
|
769
|
+
const remove = useCallback(
|
|
770
|
+
async (id) => {
|
|
771
|
+
await client.deleteApplication(id);
|
|
772
|
+
setApps((prev) => prev.filter((a) => a.id !== id));
|
|
773
|
+
},
|
|
774
|
+
[client]
|
|
775
|
+
);
|
|
776
|
+
const regenerateSecret = useCallback(
|
|
777
|
+
async (id) => {
|
|
778
|
+
const { app } = await client.regenerateSecret(id);
|
|
779
|
+
setApps((prev) => prev.map((a) => a.id === id ? app : a));
|
|
780
|
+
return app;
|
|
781
|
+
},
|
|
782
|
+
[client]
|
|
783
|
+
);
|
|
784
|
+
const getById = useCallback(
|
|
785
|
+
async (id) => {
|
|
786
|
+
const { app } = await client.getApplication(id);
|
|
787
|
+
return app;
|
|
788
|
+
},
|
|
789
|
+
[client]
|
|
790
|
+
);
|
|
791
|
+
return { apps, loading, error, refresh, create, update, remove, regenerateSecret, getById };
|
|
792
|
+
}
|
|
793
|
+
var TOKEN_STORAGE_KEY = "ark-notify-js-token";
|
|
794
|
+
function usePlatformAuth(options = {}) {
|
|
795
|
+
const { client } = useArkNotify();
|
|
796
|
+
const storageKey = options.storageKey ?? TOKEN_STORAGE_KEY;
|
|
797
|
+
const persist = options.persist ?? true;
|
|
798
|
+
const [user, setUser] = useState(null);
|
|
799
|
+
const [token, setToken] = useState(() => {
|
|
800
|
+
if (!persist || typeof window === "undefined") return null;
|
|
801
|
+
return localStorage.getItem(storageKey);
|
|
802
|
+
});
|
|
803
|
+
const [loading, setLoading] = useState(false);
|
|
804
|
+
const [error, setError] = useState(null);
|
|
805
|
+
const saveToken = useCallback(
|
|
806
|
+
(newToken) => {
|
|
807
|
+
setToken(newToken);
|
|
808
|
+
client.setToken(newToken);
|
|
809
|
+
if (persist && typeof window !== "undefined") {
|
|
810
|
+
if (newToken) {
|
|
811
|
+
localStorage.setItem(storageKey, newToken);
|
|
812
|
+
} else {
|
|
813
|
+
localStorage.removeItem(storageKey);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
[client, persist, storageKey]
|
|
818
|
+
);
|
|
819
|
+
const refreshUser = useCallback(async () => {
|
|
820
|
+
if (!token) {
|
|
821
|
+
setUser(null);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
setLoading(true);
|
|
825
|
+
setError(null);
|
|
826
|
+
try {
|
|
827
|
+
client.setToken(token);
|
|
828
|
+
const { user: fetched } = await client.me();
|
|
829
|
+
setUser(fetched);
|
|
830
|
+
} catch (err) {
|
|
831
|
+
setUser(null);
|
|
832
|
+
if (err instanceof ArkNotifyError) {
|
|
833
|
+
setError(err);
|
|
834
|
+
if (err.status === 401) saveToken(null);
|
|
835
|
+
}
|
|
836
|
+
} finally {
|
|
837
|
+
setLoading(false);
|
|
838
|
+
}
|
|
839
|
+
}, [client, token, saveToken]);
|
|
840
|
+
useEffect(() => {
|
|
841
|
+
if (token) {
|
|
842
|
+
client.setToken(token);
|
|
843
|
+
void refreshUser();
|
|
844
|
+
}
|
|
845
|
+
}, []);
|
|
846
|
+
const login = useCallback(
|
|
847
|
+
async (input) => {
|
|
848
|
+
setLoading(true);
|
|
849
|
+
setError(null);
|
|
850
|
+
try {
|
|
851
|
+
const res = await client.login(input);
|
|
852
|
+
saveToken(res.token);
|
|
853
|
+
setUser(res.user);
|
|
854
|
+
} catch (err) {
|
|
855
|
+
if (err instanceof ArkNotifyError) setError(err);
|
|
856
|
+
throw err;
|
|
857
|
+
} finally {
|
|
858
|
+
setLoading(false);
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
[client, saveToken]
|
|
862
|
+
);
|
|
863
|
+
const logout = useCallback(() => {
|
|
864
|
+
saveToken(null);
|
|
865
|
+
setUser(null);
|
|
866
|
+
setError(null);
|
|
867
|
+
}, [saveToken]);
|
|
868
|
+
return {
|
|
869
|
+
user,
|
|
870
|
+
token,
|
|
871
|
+
loading,
|
|
872
|
+
error,
|
|
873
|
+
login,
|
|
874
|
+
logout,
|
|
875
|
+
refreshUser,
|
|
876
|
+
isAuthenticated: !!user && !!token
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function useSSE(options) {
|
|
880
|
+
const { createSSE } = useArkNotify();
|
|
881
|
+
const { enabled = true, onEvent, ...sseConfig } = options;
|
|
882
|
+
const sseRef = useRef(null);
|
|
883
|
+
const [connected, setConnected] = useState(false);
|
|
884
|
+
const [connectedMessage, setConnectedMessage] = useState(null);
|
|
885
|
+
if (!sseRef.current) {
|
|
886
|
+
sseRef.current = createSSE(sseConfig);
|
|
887
|
+
}
|
|
888
|
+
const sse = sseRef.current;
|
|
889
|
+
useEffect(() => {
|
|
890
|
+
if (!enabled) return;
|
|
891
|
+
const unsubs = [
|
|
892
|
+
sse.on("connected", (msg) => {
|
|
893
|
+
setConnected(true);
|
|
894
|
+
setConnectedMessage(msg);
|
|
895
|
+
}),
|
|
896
|
+
sse.on("close", () => {
|
|
897
|
+
setConnected(false);
|
|
898
|
+
setConnectedMessage(null);
|
|
899
|
+
}),
|
|
900
|
+
onEvent ? sse.on("event", (msg) => onEvent(msg.event, msg.data, msg)) : () => {
|
|
901
|
+
}
|
|
902
|
+
];
|
|
903
|
+
void sse.connect();
|
|
904
|
+
return () => {
|
|
905
|
+
unsubs.forEach((u) => u());
|
|
906
|
+
sse.disconnect();
|
|
907
|
+
setConnected(false);
|
|
908
|
+
};
|
|
909
|
+
}, [sse, enabled, onEvent]);
|
|
910
|
+
const connect = useCallback(() => void sse.connect(), [sse]);
|
|
911
|
+
const disconnect = useCallback(() => sse.disconnect(), [sse]);
|
|
912
|
+
const bind = useCallback(
|
|
913
|
+
(channel, event, handler) => sse.bind(channel, event, (data) => handler(data)),
|
|
914
|
+
[sse]
|
|
915
|
+
);
|
|
916
|
+
return {
|
|
917
|
+
sse,
|
|
918
|
+
connected,
|
|
919
|
+
connectionId: sse.getConnectionId(),
|
|
920
|
+
connectedMessage,
|
|
921
|
+
connect,
|
|
922
|
+
disconnect,
|
|
923
|
+
bind
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
function useAdminChannels() {
|
|
927
|
+
const { client } = useArkNotify();
|
|
928
|
+
const [data, setData] = useState(null);
|
|
929
|
+
const [loading, setLoading] = useState(true);
|
|
930
|
+
const [error, setError] = useState(null);
|
|
931
|
+
const refresh = useCallback(async () => {
|
|
932
|
+
setLoading(true);
|
|
933
|
+
setError(null);
|
|
934
|
+
try {
|
|
935
|
+
const result = await client.adminChannels();
|
|
936
|
+
setData(result);
|
|
937
|
+
} catch (err) {
|
|
938
|
+
if (err instanceof ArkNotifyError) setError(err);
|
|
939
|
+
} finally {
|
|
940
|
+
setLoading(false);
|
|
941
|
+
}
|
|
942
|
+
}, [client]);
|
|
943
|
+
useEffect(() => {
|
|
944
|
+
void refresh();
|
|
945
|
+
}, [refresh]);
|
|
946
|
+
return { data, loading, error, refresh };
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export { ArkNotifyProvider, DEFAULT_BASE_URL, configureArkNotify, useAdminChannels, useApplications, useArkNotify, useChannel, useConnection, usePlatformAuth, usePresence, useSSE };
|
|
950
|
+
//# sourceMappingURL=index.js.map
|
|
951
|
+
//# sourceMappingURL=index.js.map
|