@flamingo-stack/openframe-frontend-core 0.0.212 → 0.0.213
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/dist/{chunk-ZFBLC5GV.cjs → chunk-35XIT2CF.cjs} +17 -17
- package/dist/{chunk-ZFBLC5GV.cjs.map → chunk-35XIT2CF.cjs.map} +1 -1
- package/dist/{chunk-QKFBZLIR.js → chunk-3JWIJJ44.js} +2 -2
- package/dist/chunk-CZR7ARBA.js +698 -0
- package/dist/chunk-CZR7ARBA.js.map +1 -0
- package/dist/{chunk-UYQOPC57.js → chunk-HICZPTRR.js} +4 -351
- package/dist/chunk-HICZPTRR.js.map +1 -0
- package/dist/{chunk-5BNWGK6D.js → chunk-IK2X5YJU.js} +3 -3
- package/dist/{chunk-VTUIMMHO.cjs → chunk-OTKJASSX.cjs} +26 -26
- package/dist/{chunk-VTUIMMHO.cjs.map → chunk-OTKJASSX.cjs.map} +1 -1
- package/dist/chunk-OZ3GH6OQ.cjs +698 -0
- package/dist/chunk-OZ3GH6OQ.cjs.map +1 -0
- package/dist/{chunk-EH3RWVF3.cjs → chunk-WT5JV2GS.cjs} +8 -355
- package/dist/chunk-WT5JV2GS.cjs.map +1 -0
- package/dist/{chunk-WI76ZUBE.cjs → chunk-ZDF6F7ED.cjs} +544 -678
- package/dist/chunk-ZDF6F7ED.cjs.map +1 -0
- package/dist/{chunk-3E5ANY55.js → chunk-ZTJVRSN5.js} +409 -543
- package/dist/{chunk-3E5ANY55.js.map → chunk-ZTJVRSN5.js.map} +1 -1
- package/dist/components/chat/hooks/use-jetstream-dialog-subscription.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-dialog-subscription.d.ts +0 -9
- package/dist/components/chat/hooks/use-nats-dialog-subscription.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +5 -4
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +4 -3
- package/dist/components/contact/index.cjs +6 -5
- package/dist/components/contact/index.cjs.map +1 -1
- package/dist/components/contact/index.js +5 -4
- package/dist/components/features/index.cjs +5 -4
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +4 -3
- package/dist/components/features/notifications/index.d.ts +2 -2
- package/dist/components/features/notifications/index.d.ts.map +1 -1
- package/dist/components/features/notifications/notifications-context.d.ts +16 -1
- package/dist/components/features/notifications/notifications-context.d.ts.map +1 -1
- package/dist/components/features/notifications/types.d.ts +4 -0
- package/dist/components/features/notifications/types.d.ts.map +1 -1
- package/dist/components/index.cjs +55 -54
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +7 -6
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/app-header.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +5 -4
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.js +4 -3
- package/dist/components/tickets/index.cjs +66 -65
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +6 -5
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/ui/index.cjs +5 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +4 -3
- package/dist/embed-shims/index.cjs +3 -3
- package/dist/embed-shims/index.cjs.map +1 -1
- package/dist/embed-shims/index.js +4 -4
- package/dist/hooks/index.cjs +3 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +2 -1
- package/dist/index.cjs +5 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -3
- package/dist/nats/index.cjs +28 -346
- package/dist/nats/index.cjs.map +1 -1
- package/dist/nats/index.d.ts +3 -0
- package/dist/nats/index.d.ts.map +1 -1
- package/dist/nats/index.js +30 -346
- package/dist/nats/index.js.map +1 -1
- package/dist/nats/nats-provider.d.ts +28 -0
- package/dist/nats/nats-provider.d.ts.map +1 -0
- package/dist/nats/nats.d.ts +1 -0
- package/dist/nats/nats.d.ts.map +1 -1
- package/dist/nats/shared-connection.d.ts +73 -0
- package/dist/nats/shared-connection.d.ts.map +1 -0
- package/dist/nats/use-nats-subscription.d.ts +18 -0
- package/dist/nats/use-nats-subscription.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/chat/hooks/use-jetstream-dialog-subscription.ts +60 -207
- package/src/components/chat/hooks/use-nats-dialog-subscription.ts +71 -214
- package/src/components/features/notifications/index.ts +2 -1
- package/src/components/features/notifications/notifications-context.tsx +104 -6
- package/src/components/features/notifications/types.ts +5 -0
- package/src/components/navigation/app-header.tsx +7 -9
- package/src/nats/index.ts +3 -0
- package/src/nats/nats-provider.tsx +146 -0
- package/src/nats/nats.ts +2 -0
- package/src/nats/shared-connection.ts +285 -0
- package/src/nats/use-nats-subscription.ts +99 -0
- package/dist/chunk-EH3RWVF3.cjs.map +0 -1
- package/dist/chunk-UYQOPC57.js.map +0 -1
- package/dist/chunk-WI76ZUBE.cjs.map +0 -1
- /package/dist/{chunk-QKFBZLIR.js.map → chunk-3JWIJJ44.js.map} +0 -0
- /package/dist/{chunk-5BNWGK6D.js.map → chunk-IK2X5YJU.js.map} +0 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/nats/nats.ts
|
|
4
|
+
function assertClientSide() {
|
|
5
|
+
if (typeof window === "undefined") {
|
|
6
|
+
throw new Error("NATS client can only connect from the browser/runtime with WebSocket support (window is undefined).");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
async function importNats() {
|
|
10
|
+
return await import("nats.ws");
|
|
11
|
+
}
|
|
12
|
+
function toNatsHeaders(nats, init) {
|
|
13
|
+
if (!init) return void 0;
|
|
14
|
+
if (typeof init.get === "function") return init;
|
|
15
|
+
const h = nats.headers();
|
|
16
|
+
for (const [k, v] of Object.entries(init)) {
|
|
17
|
+
if (v !== void 0 && v !== null) h.set(k, String(v));
|
|
18
|
+
}
|
|
19
|
+
return h;
|
|
20
|
+
}
|
|
21
|
+
function cryptoRandom() {
|
|
22
|
+
const buf = new Uint32Array(1);
|
|
23
|
+
crypto.getRandomValues(buf);
|
|
24
|
+
return buf[0] / (4294967295 + 1);
|
|
25
|
+
}
|
|
26
|
+
function createExponentialBackoffHandler(opts) {
|
|
27
|
+
const initialDelay = opts.initialDelayMs ?? 1e3;
|
|
28
|
+
const maxDelay = opts.maxDelayMs ?? 3e4;
|
|
29
|
+
const multiplier = opts.multiplier ?? 2;
|
|
30
|
+
const jitter = opts.jitter ?? true;
|
|
31
|
+
let attempt = 0;
|
|
32
|
+
return {
|
|
33
|
+
handler: () => {
|
|
34
|
+
const delay = Math.min(initialDelay * multiplier ** attempt, maxDelay);
|
|
35
|
+
attempt++;
|
|
36
|
+
return jitter ? delay * (0.5 + cryptoRandom() * 0.5) : delay;
|
|
37
|
+
},
|
|
38
|
+
reset: () => {
|
|
39
|
+
attempt = 0;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function mapOptionsToConnectionOptions(opts, backoff) {
|
|
44
|
+
return {
|
|
45
|
+
servers: opts.servers,
|
|
46
|
+
name: opts.name,
|
|
47
|
+
token: opts.token,
|
|
48
|
+
user: opts.user,
|
|
49
|
+
pass: opts.pass,
|
|
50
|
+
timeout: opts.connectTimeoutMs ?? 15e3,
|
|
51
|
+
reconnect: opts.reconnect ?? true,
|
|
52
|
+
maxReconnectAttempts: opts.maxReconnectAttempts,
|
|
53
|
+
reconnectTimeWait: opts.reconnectTimeWaitMs,
|
|
54
|
+
reconnectDelayHandler: backoff?.handler,
|
|
55
|
+
pingInterval: opts.pingIntervalMs,
|
|
56
|
+
maxPingOut: opts.maxPingOut,
|
|
57
|
+
inboxPrefix: opts.inboxPrefix
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function mapNatsTypeToStatus(type) {
|
|
61
|
+
const t = String(type).toLowerCase();
|
|
62
|
+
if (t.includes("disconnect")) return "disconnected";
|
|
63
|
+
if (t === "reconnecting") return "reconnecting";
|
|
64
|
+
if (t === "reconnect" || t.includes("connect")) return "connected";
|
|
65
|
+
if (t.includes("error")) return "error";
|
|
66
|
+
if (t.includes("close")) return "closed";
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function createNatsClient(options) {
|
|
70
|
+
let nc = null;
|
|
71
|
+
let statusLoopAbort = null;
|
|
72
|
+
let connectInFlight = null;
|
|
73
|
+
const backoff = options.exponentialBackoff ? createExponentialBackoffHandler(options.exponentialBackoff) : void 0;
|
|
74
|
+
const statusListeners = /* @__PURE__ */ new Set();
|
|
75
|
+
function emitStatus(event) {
|
|
76
|
+
for (const listener of statusListeners) {
|
|
77
|
+
try {
|
|
78
|
+
listener(event);
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function connect() {
|
|
84
|
+
if (nc && !nc.isClosed()) return;
|
|
85
|
+
if (connectInFlight) return connectInFlight;
|
|
86
|
+
assertClientSide();
|
|
87
|
+
connectInFlight = (async () => {
|
|
88
|
+
try {
|
|
89
|
+
emitStatus({ status: "connecting" });
|
|
90
|
+
const nats = await importNats();
|
|
91
|
+
const conn = await nats.connect(mapOptionsToConnectionOptions(options, backoff));
|
|
92
|
+
nc = conn;
|
|
93
|
+
emitStatus({ status: "connected" });
|
|
94
|
+
statusLoopAbort = new AbortController();
|
|
95
|
+
const signal = statusLoopAbort.signal;
|
|
96
|
+
(async () => {
|
|
97
|
+
try {
|
|
98
|
+
for await (const s of conn.status()) {
|
|
99
|
+
if (signal.aborted) return;
|
|
100
|
+
const mapped = mapNatsTypeToStatus(s?.type);
|
|
101
|
+
if (mapped) {
|
|
102
|
+
if (mapped === "connected" && backoff) {
|
|
103
|
+
backoff.reset();
|
|
104
|
+
}
|
|
105
|
+
emitStatus({ status: mapped, data: s?.data });
|
|
106
|
+
if (mapped === "closed") {
|
|
107
|
+
nc = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
if (!signal.aborted) {
|
|
113
|
+
emitStatus({ status: "error", data: e });
|
|
114
|
+
nc = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
})().catch(() => {
|
|
118
|
+
});
|
|
119
|
+
} finally {
|
|
120
|
+
connectInFlight = null;
|
|
121
|
+
}
|
|
122
|
+
})();
|
|
123
|
+
return connectInFlight;
|
|
124
|
+
}
|
|
125
|
+
async function close() {
|
|
126
|
+
const conn = nc;
|
|
127
|
+
nc = null;
|
|
128
|
+
if (statusLoopAbort) {
|
|
129
|
+
try {
|
|
130
|
+
statusLoopAbort.abort();
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
statusLoopAbort = null;
|
|
134
|
+
}
|
|
135
|
+
if (!conn) return;
|
|
136
|
+
try {
|
|
137
|
+
await conn.drain();
|
|
138
|
+
} finally {
|
|
139
|
+
try {
|
|
140
|
+
await conn.close();
|
|
141
|
+
} finally {
|
|
142
|
+
emitStatus({ status: "closed" });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function requireConnection() {
|
|
147
|
+
if (!nc) throw new Error("NATS is not connected. Call client.connect() first.");
|
|
148
|
+
return nc;
|
|
149
|
+
}
|
|
150
|
+
function isConnected() {
|
|
151
|
+
return Boolean(nc) && !nc.isClosed();
|
|
152
|
+
}
|
|
153
|
+
function publishBytes(subject, payload, opts) {
|
|
154
|
+
const conn = requireConnection();
|
|
155
|
+
(async () => {
|
|
156
|
+
const nats = await importNats();
|
|
157
|
+
conn.publish(subject, payload, { headers: toNatsHeaders(nats, opts?.headers) });
|
|
158
|
+
})().catch((e) => emitStatus({ status: "error", data: e }));
|
|
159
|
+
}
|
|
160
|
+
function publishString(subject, payload, opts) {
|
|
161
|
+
;
|
|
162
|
+
(async () => {
|
|
163
|
+
const nats = await importNats();
|
|
164
|
+
const sc = nats.StringCodec();
|
|
165
|
+
publishBytes(subject, sc.encode(payload), opts);
|
|
166
|
+
})().catch((e) => emitStatus({ status: "error", data: e }));
|
|
167
|
+
}
|
|
168
|
+
function publishJson(subject, payload, opts) {
|
|
169
|
+
;
|
|
170
|
+
(async () => {
|
|
171
|
+
const nats = await importNats();
|
|
172
|
+
const jc = nats.JSONCodec();
|
|
173
|
+
publishBytes(subject, jc.encode(payload), opts);
|
|
174
|
+
})().catch((e) => emitStatus({ status: "error", data: e }));
|
|
175
|
+
}
|
|
176
|
+
async function requestBytes(subject, payload, opts) {
|
|
177
|
+
const conn = requireConnection();
|
|
178
|
+
const nats = await importNats();
|
|
179
|
+
const msg = await conn.request(subject, payload, {
|
|
180
|
+
timeout: opts?.timeoutMs ?? 2e3,
|
|
181
|
+
headers: toNatsHeaders(nats, opts?.headers)
|
|
182
|
+
});
|
|
183
|
+
return msg;
|
|
184
|
+
}
|
|
185
|
+
async function requestString(subject, payload, opts) {
|
|
186
|
+
const nats = await importNats();
|
|
187
|
+
const sc = nats.StringCodec();
|
|
188
|
+
const msg = await requestBytes(subject, sc.encode(payload), opts);
|
|
189
|
+
return sc.decode(msg.data);
|
|
190
|
+
}
|
|
191
|
+
async function requestJson(subject, payload, opts) {
|
|
192
|
+
const nats = await importNats();
|
|
193
|
+
const reqCodec = nats.JSONCodec();
|
|
194
|
+
const resCodec = nats.JSONCodec();
|
|
195
|
+
const msg = await requestBytes(subject, reqCodec.encode(payload), opts);
|
|
196
|
+
return resCodec.decode(msg.data);
|
|
197
|
+
}
|
|
198
|
+
function subscribeBytes(subject, onMessage, opts) {
|
|
199
|
+
const conn = requireConnection();
|
|
200
|
+
const sub = conn.subscribe(subject, { queue: opts?.queue });
|
|
201
|
+
if (typeof opts?.max === "number") sub.unsubscribe(opts.max);
|
|
202
|
+
const abortController = new AbortController();
|
|
203
|
+
const signal = opts?.signal ?? abortController.signal;
|
|
204
|
+
(async () => {
|
|
205
|
+
try {
|
|
206
|
+
for await (const msg of sub) {
|
|
207
|
+
if (signal.aborted) break;
|
|
208
|
+
await onMessage(msg);
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
emitStatus({ status: "error", data: e });
|
|
212
|
+
} finally {
|
|
213
|
+
try {
|
|
214
|
+
sub.unsubscribe();
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
})().catch((e) => emitStatus({ status: "error", data: e }));
|
|
219
|
+
return {
|
|
220
|
+
subscription: sub,
|
|
221
|
+
unsubscribe() {
|
|
222
|
+
try {
|
|
223
|
+
abortController.abort();
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
sub.unsubscribe();
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function subscribeString(subject, onMessage, opts) {
|
|
234
|
+
return subscribeBytes(
|
|
235
|
+
subject,
|
|
236
|
+
async (msg) => {
|
|
237
|
+
const nats = await importNats();
|
|
238
|
+
const sc = nats.StringCodec();
|
|
239
|
+
await onMessage(sc.decode(msg.data), msg);
|
|
240
|
+
},
|
|
241
|
+
opts
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
function subscribeJson(subject, onMessage, opts) {
|
|
245
|
+
return subscribeBytes(
|
|
246
|
+
subject,
|
|
247
|
+
async (msg) => {
|
|
248
|
+
const nats = await importNats();
|
|
249
|
+
const jc = nats.JSONCodec();
|
|
250
|
+
await onMessage(jc.decode(msg.data), msg);
|
|
251
|
+
},
|
|
252
|
+
opts
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
async function subscribeJetStreamOrdered(onMessage, opts) {
|
|
256
|
+
const conn = requireConnection();
|
|
257
|
+
if (opts.signal?.aborted) {
|
|
258
|
+
return { unsubscribe() {
|
|
259
|
+
} };
|
|
260
|
+
}
|
|
261
|
+
const nats = await importNats();
|
|
262
|
+
if (opts.signal?.aborted) {
|
|
263
|
+
return { unsubscribe() {
|
|
264
|
+
} };
|
|
265
|
+
}
|
|
266
|
+
const inactiveThresholdNs = (opts.inactiveThresholdMs ?? 5 * 6e4) * 1e6;
|
|
267
|
+
const js = conn.jetstream();
|
|
268
|
+
const consumer = await js.consumers.get(opts.streamName, {
|
|
269
|
+
filterSubjects: opts.filterSubject,
|
|
270
|
+
deliver_policy: nats.DeliverPolicy.StartSequence,
|
|
271
|
+
opt_start_seq: opts.optStartSeq ?? 0,
|
|
272
|
+
inactive_threshold: inactiveThresholdNs
|
|
273
|
+
});
|
|
274
|
+
const iterRef = { current: null };
|
|
275
|
+
let closed = false;
|
|
276
|
+
const onAbort = () => {
|
|
277
|
+
void teardown();
|
|
278
|
+
};
|
|
279
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
280
|
+
async function teardown() {
|
|
281
|
+
if (closed) return;
|
|
282
|
+
closed = true;
|
|
283
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
284
|
+
const iter = iterRef.current;
|
|
285
|
+
iterRef.current = null;
|
|
286
|
+
if (iter) {
|
|
287
|
+
try {
|
|
288
|
+
await iter.close();
|
|
289
|
+
} catch {
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (opts.signal?.aborted) {
|
|
294
|
+
void teardown();
|
|
295
|
+
return { unsubscribe() {
|
|
296
|
+
} };
|
|
297
|
+
}
|
|
298
|
+
;
|
|
299
|
+
(async () => {
|
|
300
|
+
try {
|
|
301
|
+
const iter = await consumer.consume();
|
|
302
|
+
if (closed) {
|
|
303
|
+
try {
|
|
304
|
+
await iter.close();
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
iterRef.current = iter;
|
|
310
|
+
for await (const msg of iter) {
|
|
311
|
+
if (closed) break;
|
|
312
|
+
try {
|
|
313
|
+
await onMessage(msg);
|
|
314
|
+
} catch (e) {
|
|
315
|
+
emitStatus({ status: "error", data: e });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} catch (e) {
|
|
319
|
+
if (!closed) emitStatus({ status: "error", data: e });
|
|
320
|
+
}
|
|
321
|
+
})().catch((e) => emitStatus({ status: "error", data: e }));
|
|
322
|
+
return {
|
|
323
|
+
unsubscribe() {
|
|
324
|
+
void teardown();
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function onStatus(listener) {
|
|
329
|
+
statusListeners.add(listener);
|
|
330
|
+
return () => statusListeners.delete(listener);
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
connect,
|
|
334
|
+
close,
|
|
335
|
+
isConnected,
|
|
336
|
+
publishBytes,
|
|
337
|
+
publishString,
|
|
338
|
+
publishJson,
|
|
339
|
+
requestBytes,
|
|
340
|
+
requestString,
|
|
341
|
+
requestJson,
|
|
342
|
+
subscribeBytes,
|
|
343
|
+
subscribeString,
|
|
344
|
+
subscribeJson,
|
|
345
|
+
subscribeJetStreamOrdered,
|
|
346
|
+
onStatus
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/nats/shared-connection.ts
|
|
351
|
+
var NATS_DEFAULTS = {
|
|
352
|
+
SHARED_CLOSE_DELAY_MS: 3e3,
|
|
353
|
+
CONNECT_TIMEOUT_MS: 1e4,
|
|
354
|
+
PING_INTERVAL_MS: 3e4,
|
|
355
|
+
MAX_PING_OUT: 3,
|
|
356
|
+
RETRY_INITIAL_DELAY_MS: 1e3,
|
|
357
|
+
RETRY_MAX_DELAY_MS: 3e4,
|
|
358
|
+
RETRY_MULTIPLIER: 2
|
|
359
|
+
};
|
|
360
|
+
var shared = null;
|
|
361
|
+
function getSharedConnection() {
|
|
362
|
+
return shared;
|
|
363
|
+
}
|
|
364
|
+
function acquireClient(url, opts) {
|
|
365
|
+
if (shared?.wsUrl !== url) {
|
|
366
|
+
if (shared) {
|
|
367
|
+
if (shared.closeTimer) clearTimeout(shared.closeTimer);
|
|
368
|
+
const old = shared;
|
|
369
|
+
shared = null;
|
|
370
|
+
void old.client.close().catch(() => {
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const {
|
|
374
|
+
name = "openframe-frontend",
|
|
375
|
+
user = "machine",
|
|
376
|
+
pass = "",
|
|
377
|
+
connectTimeoutMs = NATS_DEFAULTS.CONNECT_TIMEOUT_MS,
|
|
378
|
+
pingIntervalMs = NATS_DEFAULTS.PING_INTERVAL_MS,
|
|
379
|
+
maxPingOut = NATS_DEFAULTS.MAX_PING_OUT
|
|
380
|
+
} = opts ?? {};
|
|
381
|
+
const client = createNatsClient({
|
|
382
|
+
servers: url,
|
|
383
|
+
name,
|
|
384
|
+
user,
|
|
385
|
+
pass,
|
|
386
|
+
connectTimeoutMs,
|
|
387
|
+
reconnect: false,
|
|
388
|
+
pingIntervalMs,
|
|
389
|
+
maxPingOut
|
|
390
|
+
});
|
|
391
|
+
shared = {
|
|
392
|
+
wsUrl: url,
|
|
393
|
+
client,
|
|
394
|
+
connectPromise: null,
|
|
395
|
+
refCount: 0,
|
|
396
|
+
closeTimer: null,
|
|
397
|
+
retryTimer: null,
|
|
398
|
+
retryOwner: null
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
shared.refCount += 1;
|
|
402
|
+
if (shared.closeTimer) {
|
|
403
|
+
clearTimeout(shared.closeTimer);
|
|
404
|
+
shared.closeTimer = null;
|
|
405
|
+
}
|
|
406
|
+
return shared;
|
|
407
|
+
}
|
|
408
|
+
function releaseClient(url, opts) {
|
|
409
|
+
if (!shared || shared.wsUrl !== url) return;
|
|
410
|
+
shared.refCount = Math.max(0, shared.refCount - 1);
|
|
411
|
+
if (shared.refCount > 0) return;
|
|
412
|
+
const delay = opts?.delayMs ?? NATS_DEFAULTS.SHARED_CLOSE_DELAY_MS;
|
|
413
|
+
shared.closeTimer = setTimeout(() => {
|
|
414
|
+
const s = shared;
|
|
415
|
+
shared = null;
|
|
416
|
+
if (s) {
|
|
417
|
+
if (s.retryTimer) {
|
|
418
|
+
clearTimeout(s.retryTimer);
|
|
419
|
+
s.retryTimer = null;
|
|
420
|
+
}
|
|
421
|
+
void s.client.close().catch(() => {
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}, delay);
|
|
425
|
+
}
|
|
426
|
+
function getSharedConnectionFor(url) {
|
|
427
|
+
if (!url) return null;
|
|
428
|
+
return shared && shared.wsUrl === url ? shared : null;
|
|
429
|
+
}
|
|
430
|
+
var defaultShouldRetryOn = (status) => status === "closed" || status === "disconnected";
|
|
431
|
+
function startConnectionLifecycle(options) {
|
|
432
|
+
const { conn, wsUrl } = options;
|
|
433
|
+
const ownerToken = {};
|
|
434
|
+
if (!conn.retryOwner) conn.retryOwner = ownerToken;
|
|
435
|
+
let closed = false;
|
|
436
|
+
let retryAttempt = 0;
|
|
437
|
+
function emitSynthetic(status) {
|
|
438
|
+
if (closed) return;
|
|
439
|
+
options.onStatusChange?.(status, { status });
|
|
440
|
+
if (status === "connected") {
|
|
441
|
+
retryAttempt = 0;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function scheduleRetry() {
|
|
445
|
+
if (closed) return;
|
|
446
|
+
if (getSharedConnectionFor(wsUrl) !== conn) return;
|
|
447
|
+
if (!conn.retryOwner) conn.retryOwner = ownerToken;
|
|
448
|
+
if (conn.retryOwner !== ownerToken) return;
|
|
449
|
+
if (conn.retryTimer) {
|
|
450
|
+
clearTimeout(conn.retryTimer);
|
|
451
|
+
conn.retryTimer = null;
|
|
452
|
+
}
|
|
453
|
+
const cfg = options.backoff ?? {};
|
|
454
|
+
const fastRetries = cfg.fastRetries ?? 0;
|
|
455
|
+
const fastDelay = cfg.fastRetryDelayMs ?? NATS_DEFAULTS.RETRY_INITIAL_DELAY_MS;
|
|
456
|
+
const baseDelay = cfg.initialDelayMs ?? NATS_DEFAULTS.RETRY_INITIAL_DELAY_MS;
|
|
457
|
+
const maxDelay = cfg.maxDelayMs ?? NATS_DEFAULTS.RETRY_MAX_DELAY_MS;
|
|
458
|
+
const multiplier = cfg.multiplier ?? NATS_DEFAULTS.RETRY_MULTIPLIER;
|
|
459
|
+
const delay = retryAttempt < fastRetries ? fastDelay : Math.min(baseDelay * multiplier ** (retryAttempt - fastRetries), maxDelay);
|
|
460
|
+
const jitteredDelay = delay * (0.5 + Math.random() * 0.5);
|
|
461
|
+
retryAttempt++;
|
|
462
|
+
conn.retryTimer = setTimeout(async () => {
|
|
463
|
+
conn.retryTimer = null;
|
|
464
|
+
if (closed) return;
|
|
465
|
+
if (getSharedConnectionFor(wsUrl) !== conn) return;
|
|
466
|
+
try {
|
|
467
|
+
await options.onBeforeReconnect?.();
|
|
468
|
+
} catch {
|
|
469
|
+
}
|
|
470
|
+
if (closed) return;
|
|
471
|
+
if (getSharedConnectionFor(wsUrl) !== conn) return;
|
|
472
|
+
const freshUrl = options.getFreshUrl();
|
|
473
|
+
if (freshUrl !== wsUrl) return;
|
|
474
|
+
try {
|
|
475
|
+
conn.connectPromise = null;
|
|
476
|
+
conn.connectPromise = conn.client.connect();
|
|
477
|
+
await conn.connectPromise;
|
|
478
|
+
if (!closed && getSharedConnectionFor(wsUrl) === conn) {
|
|
479
|
+
retryAttempt = 0;
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
conn.connectPromise = null;
|
|
483
|
+
if (!closed && getSharedConnectionFor(wsUrl) === conn) {
|
|
484
|
+
scheduleRetry();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}, jitteredDelay);
|
|
488
|
+
}
|
|
489
|
+
const shouldRetryOn = options.shouldRetryOn ?? defaultShouldRetryOn;
|
|
490
|
+
const unsubStatus = conn.client.onStatus((evt) => {
|
|
491
|
+
if (closed) return;
|
|
492
|
+
options.onStatusChange?.(evt.status, evt);
|
|
493
|
+
if (evt.status === "connected") {
|
|
494
|
+
retryAttempt = 0;
|
|
495
|
+
}
|
|
496
|
+
if (shouldRetryOn(evt.status)) {
|
|
497
|
+
scheduleRetry();
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
if (conn.client.isConnected()) {
|
|
501
|
+
emitSynthetic("connected");
|
|
502
|
+
}
|
|
503
|
+
void (async () => {
|
|
504
|
+
try {
|
|
505
|
+
conn.connectPromise || (conn.connectPromise = conn.client.connect());
|
|
506
|
+
await conn.connectPromise;
|
|
507
|
+
} catch {
|
|
508
|
+
conn.connectPromise = null;
|
|
509
|
+
if (closed) return;
|
|
510
|
+
emitSynthetic("disconnected");
|
|
511
|
+
scheduleRetry();
|
|
512
|
+
}
|
|
513
|
+
})();
|
|
514
|
+
return {
|
|
515
|
+
stop() {
|
|
516
|
+
closed = true;
|
|
517
|
+
unsubStatus();
|
|
518
|
+
if (conn.retryTimer) {
|
|
519
|
+
clearTimeout(conn.retryTimer);
|
|
520
|
+
conn.retryTimer = null;
|
|
521
|
+
}
|
|
522
|
+
if (conn.retryOwner === ownerToken) {
|
|
523
|
+
conn.retryOwner = null;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/nats/nats-provider.tsx
|
|
530
|
+
import * as React from "react";
|
|
531
|
+
import { jsx } from "react/jsx-runtime";
|
|
532
|
+
var NatsContext = React.createContext(null);
|
|
533
|
+
function NatsProvider({
|
|
534
|
+
children,
|
|
535
|
+
getWsUrl,
|
|
536
|
+
onBeforeReconnect,
|
|
537
|
+
clientConfig,
|
|
538
|
+
reconnectionBackoff,
|
|
539
|
+
urlRevision
|
|
540
|
+
}) {
|
|
541
|
+
const [client, setClient] = React.useState(null);
|
|
542
|
+
const [status, setStatus] = React.useState("closed");
|
|
543
|
+
const [reconnectionCount, setReconnectionCount] = React.useState(0);
|
|
544
|
+
const getWsUrlRef = React.useRef(getWsUrl);
|
|
545
|
+
React.useEffect(() => {
|
|
546
|
+
getWsUrlRef.current = getWsUrl;
|
|
547
|
+
}, [getWsUrl]);
|
|
548
|
+
const onBeforeReconnectRef = React.useRef(onBeforeReconnect);
|
|
549
|
+
React.useEffect(() => {
|
|
550
|
+
onBeforeReconnectRef.current = onBeforeReconnect;
|
|
551
|
+
}, [onBeforeReconnect]);
|
|
552
|
+
const reconnectionBackoffRef = React.useRef(reconnectionBackoff);
|
|
553
|
+
React.useEffect(() => {
|
|
554
|
+
reconnectionBackoffRef.current = reconnectionBackoff;
|
|
555
|
+
}, [reconnectionBackoff]);
|
|
556
|
+
const clientConfigRef = React.useRef(clientConfig);
|
|
557
|
+
React.useEffect(() => {
|
|
558
|
+
clientConfigRef.current = clientConfig;
|
|
559
|
+
}, [clientConfig]);
|
|
560
|
+
const heldUrlRef = React.useRef(null);
|
|
561
|
+
const hadConnectionBeforeRef = React.useRef(false);
|
|
562
|
+
React.useEffect(() => {
|
|
563
|
+
const wsUrl = getWsUrlRef.current();
|
|
564
|
+
if (!wsUrl) {
|
|
565
|
+
if (heldUrlRef.current) {
|
|
566
|
+
releaseClient(heldUrlRef.current);
|
|
567
|
+
heldUrlRef.current = null;
|
|
568
|
+
setClient(null);
|
|
569
|
+
setStatus("closed");
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (heldUrlRef.current && heldUrlRef.current !== wsUrl) {
|
|
574
|
+
releaseClient(heldUrlRef.current);
|
|
575
|
+
heldUrlRef.current = null;
|
|
576
|
+
}
|
|
577
|
+
const conn = acquireClient(wsUrl, clientConfigRef.current);
|
|
578
|
+
heldUrlRef.current = wsUrl;
|
|
579
|
+
setClient(conn.client);
|
|
580
|
+
setStatus(conn.client.isConnected() ? "connected" : "connecting");
|
|
581
|
+
const lifecycle = startConnectionLifecycle({
|
|
582
|
+
conn,
|
|
583
|
+
wsUrl,
|
|
584
|
+
onBeforeReconnect: () => onBeforeReconnectRef.current?.(),
|
|
585
|
+
backoff: reconnectionBackoffRef.current,
|
|
586
|
+
getFreshUrl: () => getWsUrlRef.current(),
|
|
587
|
+
onStatusChange: (newStatus) => {
|
|
588
|
+
setStatus(newStatus);
|
|
589
|
+
if (newStatus === "connected") {
|
|
590
|
+
if (hadConnectionBeforeRef.current) {
|
|
591
|
+
setReconnectionCount((c) => c + 1);
|
|
592
|
+
}
|
|
593
|
+
hadConnectionBeforeRef.current = true;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
return () => {
|
|
598
|
+
lifecycle.stop();
|
|
599
|
+
if (heldUrlRef.current) {
|
|
600
|
+
releaseClient(heldUrlRef.current);
|
|
601
|
+
heldUrlRef.current = null;
|
|
602
|
+
}
|
|
603
|
+
setClient(null);
|
|
604
|
+
setStatus("closed");
|
|
605
|
+
};
|
|
606
|
+
}, [urlRevision]);
|
|
607
|
+
const value = React.useMemo(
|
|
608
|
+
() => ({
|
|
609
|
+
client,
|
|
610
|
+
status,
|
|
611
|
+
isReady: status === "connected" && client !== null,
|
|
612
|
+
reconnectionCount
|
|
613
|
+
}),
|
|
614
|
+
[client, status, reconnectionCount]
|
|
615
|
+
);
|
|
616
|
+
return /* @__PURE__ */ jsx(NatsContext.Provider, { value, children });
|
|
617
|
+
}
|
|
618
|
+
function useNats() {
|
|
619
|
+
const ctx = React.useContext(NatsContext);
|
|
620
|
+
if (!ctx) throw new Error("useNats must be used inside <NatsProvider>");
|
|
621
|
+
return ctx;
|
|
622
|
+
}
|
|
623
|
+
function useOptionalNats() {
|
|
624
|
+
return React.useContext(NatsContext);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/nats/use-nats-subscription.ts
|
|
628
|
+
import * as React2 from "react";
|
|
629
|
+
function useNatsSubscription(subject, onMessage, options) {
|
|
630
|
+
const nats = useOptionalNats();
|
|
631
|
+
const handlerRef = React2.useRef(onMessage);
|
|
632
|
+
React2.useEffect(() => {
|
|
633
|
+
handlerRef.current = onMessage;
|
|
634
|
+
}, [onMessage]);
|
|
635
|
+
const [isSubscribed, setIsSubscribed] = React2.useState(false);
|
|
636
|
+
const enabled = options?.enabled !== false;
|
|
637
|
+
const queue = options?.queue;
|
|
638
|
+
const max = options?.max;
|
|
639
|
+
const reconnectionCount = nats?.reconnectionCount ?? 0;
|
|
640
|
+
const isReady = !!nats?.isReady;
|
|
641
|
+
const client = nats?.client ?? null;
|
|
642
|
+
React2.useEffect(() => {
|
|
643
|
+
if (!client || !isReady || !subject || !enabled) {
|
|
644
|
+
setIsSubscribed(false);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const sub = client.subscribeBytes(
|
|
648
|
+
subject,
|
|
649
|
+
(msg) => handlerRef.current(msg),
|
|
650
|
+
{ queue, max }
|
|
651
|
+
);
|
|
652
|
+
setIsSubscribed(true);
|
|
653
|
+
return () => {
|
|
654
|
+
setIsSubscribed(false);
|
|
655
|
+
try {
|
|
656
|
+
sub.unsubscribe();
|
|
657
|
+
} catch {
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
}, [client, isReady, subject, enabled, queue, max, reconnectionCount]);
|
|
661
|
+
return { isSubscribed, isReady };
|
|
662
|
+
}
|
|
663
|
+
function useNatsJsonSubscription(subject, onPayload, options) {
|
|
664
|
+
const handlerRef = React2.useRef(onPayload);
|
|
665
|
+
React2.useEffect(() => {
|
|
666
|
+
handlerRef.current = onPayload;
|
|
667
|
+
}, [onPayload]);
|
|
668
|
+
const decoderRef = React2.useRef(null);
|
|
669
|
+
if (!decoderRef.current && typeof TextDecoder !== "undefined") {
|
|
670
|
+
decoderRef.current = new TextDecoder();
|
|
671
|
+
}
|
|
672
|
+
const wrapped = React2.useCallback(async (msg) => {
|
|
673
|
+
const decoder = decoderRef.current;
|
|
674
|
+
if (!decoder) return;
|
|
675
|
+
try {
|
|
676
|
+
const parsed = JSON.parse(decoder.decode(msg.data));
|
|
677
|
+
await handlerRef.current(parsed, msg);
|
|
678
|
+
} catch {
|
|
679
|
+
}
|
|
680
|
+
}, []);
|
|
681
|
+
return useNatsSubscription(subject, wrapped, options);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export {
|
|
685
|
+
createNatsClient,
|
|
686
|
+
NATS_DEFAULTS,
|
|
687
|
+
getSharedConnection,
|
|
688
|
+
acquireClient,
|
|
689
|
+
releaseClient,
|
|
690
|
+
getSharedConnectionFor,
|
|
691
|
+
startConnectionLifecycle,
|
|
692
|
+
NatsProvider,
|
|
693
|
+
useNats,
|
|
694
|
+
useOptionalNats,
|
|
695
|
+
useNatsSubscription,
|
|
696
|
+
useNatsJsonSubscription
|
|
697
|
+
};
|
|
698
|
+
//# sourceMappingURL=chunk-CZR7ARBA.js.map
|