@memori.ai/memori-react 8.40.4 → 8.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/components/MemoriWidget/MemoriWidget.js +123 -26
- package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/dist/helpers/nats/getNatsConfig.d.ts +4 -0
- package/dist/helpers/nats/getNatsConfig.js +42 -5
- package/dist/helpers/nats/getNatsConfig.js.map +1 -1
- package/dist/helpers/nats/isSessionExpiredError.d.ts +3 -0
- package/dist/helpers/nats/isSessionExpiredError.js +37 -0
- package/dist/helpers/nats/isSessionExpiredError.js.map +1 -0
- package/dist/helpers/nats/useNats.js +6 -2
- package/dist/helpers/nats/useNats.js.map +1 -1
- package/dist/helpers/nats/useNatsSession.d.ts +2 -1
- package/dist/helpers/nats/useNatsSession.js +122 -25
- package/dist/helpers/nats/useNatsSession.js.map +1 -1
- package/dist/locales/de.json +1 -0
- package/dist/locales/en.json +1 -0
- package/dist/locales/es.json +1 -0
- package/dist/locales/fr.json +1 -0
- package/dist/locales/it.json +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/esm/components/MemoriWidget/MemoriWidget.js +123 -26
- package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/esm/helpers/nats/getNatsConfig.d.ts +4 -0
- package/esm/helpers/nats/getNatsConfig.js +40 -4
- package/esm/helpers/nats/getNatsConfig.js.map +1 -1
- package/esm/helpers/nats/isSessionExpiredError.d.ts +3 -0
- package/esm/helpers/nats/isSessionExpiredError.js +32 -0
- package/esm/helpers/nats/isSessionExpiredError.js.map +1 -0
- package/esm/helpers/nats/useNats.js +6 -2
- package/esm/helpers/nats/useNats.js.map +1 -1
- package/esm/helpers/nats/useNatsSession.d.ts +2 -1
- package/esm/helpers/nats/useNatsSession.js +123 -26
- package/esm/helpers/nats/useNatsSession.js.map +1 -1
- package/esm/locales/de.json +1 -0
- package/esm/locales/en.json +1 -0
- package/esm/locales/es.json +1 -0
- package/esm/locales/fr.json +1 -0
- package/esm/locales/it.json +1 -0
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/package.json +2 -1
- package/src/components/MemoriWidget/MemoriWidget.tsx +195 -46
- package/src/helpers/nats/getNatsConfig.ts +83 -7
- package/src/helpers/nats/isSessionExpiredError.test.ts +38 -0
- package/src/helpers/nats/isSessionExpiredError.ts +51 -0
- package/src/helpers/nats/useNats.ts +8 -6
- package/src/helpers/nats/useNatsSession.ts +203 -39
- package/src/index.stories.tsx +16 -0
- package/src/locales/de.json +1 -0
- package/src/locales/en.json +1 -0
- package/src/locales/es.json +1 -0
- package/src/locales/fr.json +1 -0
- package/src/locales/it.json +1 -0
- package/src/version.ts +1 -1
|
@@ -9,6 +9,87 @@ export interface NatsConfig {
|
|
|
9
9
|
url: string;
|
|
10
10
|
/** Bearer token used to authenticate the WebSocket connection. */
|
|
11
11
|
token: string;
|
|
12
|
+
/**
|
|
13
|
+
* JetStream stream that stores session events. When set, the client consumes
|
|
14
|
+
* via JetStream instead of core NATS subscribe.
|
|
15
|
+
*/
|
|
16
|
+
stream?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Optional durable consumer name. When omitted, an ordered ephemeral consumer
|
|
19
|
+
* filtered to the session subject is used.
|
|
20
|
+
*/
|
|
21
|
+
consumer?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Subject to subscribe or filter on. Defaults to `sessions.<sessionId>` when omitted.
|
|
24
|
+
*/
|
|
25
|
+
subject?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readStringField(
|
|
29
|
+
raw: Record<string, unknown>,
|
|
30
|
+
...keys: string[]
|
|
31
|
+
): string | undefined {
|
|
32
|
+
for (const key of keys) {
|
|
33
|
+
const value = raw[key];
|
|
34
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readBooleanField(
|
|
42
|
+
raw: Record<string, unknown>,
|
|
43
|
+
...keys: string[]
|
|
44
|
+
): boolean | undefined {
|
|
45
|
+
for (const key of keys) {
|
|
46
|
+
const value = raw[key];
|
|
47
|
+
if (typeof value === 'boolean') {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Normalizes raw `/api/nats` payloads (camelCase or snake_case) into {@link NatsConfig}.
|
|
56
|
+
*/
|
|
57
|
+
export function parseNatsConfig(raw: Record<string, unknown>): NatsConfig {
|
|
58
|
+
const url = readStringField(raw, 'url');
|
|
59
|
+
const token = readStringField(raw, 'token');
|
|
60
|
+
|
|
61
|
+
if (!url || !token) {
|
|
62
|
+
throw new Error('Invalid response from NATS config service');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const useJetStream = readBooleanField(raw, 'useJetStream', 'use_jetstream');
|
|
66
|
+
const stream = readStringField(raw, 'stream', 'streamName', 'stream_name');
|
|
67
|
+
const consumer = readStringField(
|
|
68
|
+
raw,
|
|
69
|
+
'consumer',
|
|
70
|
+
'consumerName',
|
|
71
|
+
'consumer_name',
|
|
72
|
+
'durableName',
|
|
73
|
+
'durable_name'
|
|
74
|
+
);
|
|
75
|
+
const subject = readStringField(
|
|
76
|
+
raw,
|
|
77
|
+
'subject',
|
|
78
|
+
'filterSubject',
|
|
79
|
+
'filter_subject'
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (useJetStream && !stream) {
|
|
83
|
+
throw new Error('Invalid response from NATS config service: JetStream enabled but stream missing');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
url,
|
|
88
|
+
token,
|
|
89
|
+
...(stream ? { stream } : {}),
|
|
90
|
+
...(consumer ? { consumer } : {}),
|
|
91
|
+
...(subject ? { subject } : {}),
|
|
92
|
+
};
|
|
12
93
|
}
|
|
13
94
|
|
|
14
95
|
/**
|
|
@@ -59,11 +140,6 @@ export async function getNatsConfig(
|
|
|
59
140
|
}
|
|
60
141
|
}
|
|
61
142
|
|
|
62
|
-
const data = (await response.json()) as
|
|
63
|
-
|
|
64
|
-
if (!data.url || !data.token) {
|
|
65
|
-
throw new Error('Invalid response from NATS config service');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { url: data.url, token: data.token };
|
|
143
|
+
const data = (await response.json()) as Record<string, unknown>;
|
|
144
|
+
return parseNatsConfig(data);
|
|
69
145
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isSessionExpiredNatsError,
|
|
3
|
+
isSessionExpiredNatsResponse,
|
|
4
|
+
} from './isSessionExpiredError';
|
|
5
|
+
|
|
6
|
+
describe('isSessionExpiredNatsError', () => {
|
|
7
|
+
it('detects session not found from EnterTextAsync NATS error', () => {
|
|
8
|
+
expect(
|
|
9
|
+
isSessionExpiredNatsError({
|
|
10
|
+
eventType: 'error',
|
|
11
|
+
errorCode: 'EnterTextAsync Error',
|
|
12
|
+
errorMessage:
|
|
13
|
+
'Session with ID "c681a10b-37c1-4783-af3b-ecb592b93036" not found',
|
|
14
|
+
correlationID: 'f85a6882-92e1-4e18-9fb7-29d3bc06fd7b',
|
|
15
|
+
})
|
|
16
|
+
).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('detects SESSION_EXPIRED result code on dialog response', () => {
|
|
20
|
+
expect(
|
|
21
|
+
isSessionExpiredNatsResponse({
|
|
22
|
+
eventType: 'dialog_text_entered_response',
|
|
23
|
+
resultCode: -103,
|
|
24
|
+
correlationID: 'abc',
|
|
25
|
+
})
|
|
26
|
+
).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('ignores unrelated NATS errors', () => {
|
|
30
|
+
expect(
|
|
31
|
+
isSessionExpiredNatsError({
|
|
32
|
+
eventType: 'error',
|
|
33
|
+
errorCode: 'SomeOtherError',
|
|
34
|
+
errorMessage: 'Something went wrong',
|
|
35
|
+
})
|
|
36
|
+
).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NatsDialogResponseEvent,
|
|
3
|
+
NatsErrorEvent,
|
|
4
|
+
} from './useNatsSession';
|
|
5
|
+
|
|
6
|
+
/** Backend result codes for missing / expired sessions. */
|
|
7
|
+
const SESSION_NOT_FOUND = -101;
|
|
8
|
+
const SESSION_EXPIRED = -103;
|
|
9
|
+
|
|
10
|
+
const SESSION_NOT_FOUND_MESSAGE =
|
|
11
|
+
/session\s+with\s+id\s+["']?[^"']+["']?\s+not\s+found/i;
|
|
12
|
+
|
|
13
|
+
function isExpiredResultCode(resultCode?: number): boolean {
|
|
14
|
+
return (
|
|
15
|
+
resultCode === 404 ||
|
|
16
|
+
resultCode === SESSION_EXPIRED ||
|
|
17
|
+
resultCode === SESSION_NOT_FOUND
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isExpiredErrorMessage(message?: string): boolean {
|
|
22
|
+
if (!message) return false;
|
|
23
|
+
return SESSION_NOT_FOUND_MESSAGE.test(message);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** True when a NATS `error` event indicates the active session is gone or expired. */
|
|
27
|
+
export function isSessionExpiredNatsError(event: NatsErrorEvent): boolean {
|
|
28
|
+
if (isExpiredResultCode(event.errorCode as number | undefined)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof event.errorCode === 'string') {
|
|
33
|
+
const code = event.errorCode.toUpperCase();
|
|
34
|
+
if (code.includes('SESSION_EXPIRED') || code.includes('SESSION_NOT_FOUND')) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return isExpiredErrorMessage(event.errorMessage);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** True when a NATS dialog response reports a missing or expired session. */
|
|
43
|
+
export function isSessionExpiredNatsResponse(
|
|
44
|
+
event: NatsDialogResponseEvent
|
|
45
|
+
): boolean {
|
|
46
|
+
if (isExpiredResultCode(event.resultCode)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return isExpiredErrorMessage(event.resultMessage);
|
|
51
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// this hook *receives* asynchronous events on the session channel (progress /
|
|
5
5
|
// dialog.text_entered_response / error).
|
|
6
6
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
7
|
-
import { getNatsConfig } from './getNatsConfig';
|
|
7
|
+
import { getNatsConfig, NatsConfig } from './getNatsConfig';
|
|
8
8
|
import {
|
|
9
9
|
useNatsSession,
|
|
10
10
|
NatsSessionEvent,
|
|
@@ -39,9 +39,7 @@ export function useNats({
|
|
|
39
39
|
onDialogResponse,
|
|
40
40
|
onError,
|
|
41
41
|
}: UseNatsOptions) {
|
|
42
|
-
const [config, setConfig] = useState<
|
|
43
|
-
null
|
|
44
|
-
);
|
|
42
|
+
const [config, setConfig] = useState<NatsConfig | null>(null);
|
|
45
43
|
const [configError, setConfigError] = useState<Error | null>(null);
|
|
46
44
|
|
|
47
45
|
// Keep callbacks in refs so the dispatcher identity stays stable.
|
|
@@ -75,7 +73,11 @@ export function useNats({
|
|
|
75
73
|
getNatsConfig(baseUrl, sessionId, controller.signal)
|
|
76
74
|
.then(cfg => {
|
|
77
75
|
if (!cancelled) {
|
|
78
|
-
console.info('[NATS] config received
|
|
76
|
+
console.info('[NATS] config received', {
|
|
77
|
+
url: cfg.url,
|
|
78
|
+
jetStream: !!cfg.stream,
|
|
79
|
+
stream: cfg.stream,
|
|
80
|
+
});
|
|
79
81
|
setConfig(cfg);
|
|
80
82
|
setConfigError(null);
|
|
81
83
|
}
|
|
@@ -111,7 +113,7 @@ export function useNats({
|
|
|
111
113
|
}
|
|
112
114
|
}, []);
|
|
113
115
|
|
|
114
|
-
useNatsSession(sessionId, config
|
|
116
|
+
useNatsSession(sessionId, config ?? undefined, handleMessage);
|
|
115
117
|
|
|
116
118
|
return {
|
|
117
119
|
/** True once connection config has been retrieved. */
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
// helpers/nats/useNatsSession.ts - Subscribe to a session's NATS channel.
|
|
2
2
|
//
|
|
3
|
-
// Opens a W3C WebSocket connection (via `wsconnect`),
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
3
|
+
// Opens a W3C WebSocket connection (via `wsconnect`), then either:
|
|
4
|
+
// - core NATS: subscribes to the session subject directly, or
|
|
5
|
+
// - JetStream: consumes from the configured stream (ordered consumer by default).
|
|
6
|
+
//
|
|
7
|
+
// When JetStream is enabled, reconnects on tab visibility / network resume and
|
|
8
|
+
// replays the last message per subject to recover events missed while Safari
|
|
9
|
+
// (or other browsers) suspend the WebSocket in background.
|
|
10
|
+
//
|
|
11
|
+
// The cleanup (`consumerMessages.stop()` + `nc.close()`) is essential: without
|
|
12
|
+
// it, every `sessionId` change would leave orphan connections / iterators.
|
|
13
|
+
import { useEffect, useRef, useState, type MutableRefObject } from 'react';
|
|
8
14
|
import { wsconnect, NatsConnection } from '@nats-io/nats-core';
|
|
15
|
+
import {
|
|
16
|
+
jetstream,
|
|
17
|
+
DeliverPolicy,
|
|
18
|
+
type ConsumerMessages,
|
|
19
|
+
} from '@nats-io/jetstream';
|
|
20
|
+
import type { NatsConfig } from './getNatsConfig';
|
|
9
21
|
|
|
10
22
|
/**
|
|
11
23
|
* `progress` event: job advancement updates.
|
|
@@ -52,6 +64,8 @@ export type NatsSessionEvent =
|
|
|
52
64
|
| NatsDialogResponseEvent
|
|
53
65
|
| NatsErrorEvent;
|
|
54
66
|
|
|
67
|
+
type JetStreamConsumeMode = 'live' | 'catch-up';
|
|
68
|
+
|
|
55
69
|
function readString(
|
|
56
70
|
raw: Record<string, unknown>,
|
|
57
71
|
camelKey: string,
|
|
@@ -143,49 +157,198 @@ export function normalizeNatsEvent(raw: Record<string, unknown>): NatsSessionEve
|
|
|
143
157
|
} as NatsSessionEvent;
|
|
144
158
|
}
|
|
145
159
|
|
|
160
|
+
function decodeMessage(
|
|
161
|
+
subject: string,
|
|
162
|
+
received: number,
|
|
163
|
+
raw: Record<string, unknown>,
|
|
164
|
+
onMessage: (event: NatsSessionEvent) => void
|
|
165
|
+
) {
|
|
166
|
+
const payload = normalizeNatsEvent(raw);
|
|
167
|
+
console.debug(
|
|
168
|
+
`[NATS] message #${received} on ${subject} (eventType=${payload.eventType})`,
|
|
169
|
+
payload
|
|
170
|
+
);
|
|
171
|
+
onMessage(payload);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function consumeCoreSubscription(
|
|
175
|
+
nc: NatsConnection,
|
|
176
|
+
subject: string,
|
|
177
|
+
onMessage: (event: NatsSessionEvent) => void
|
|
178
|
+
) {
|
|
179
|
+
const sub = nc.subscribe(subject);
|
|
180
|
+
console.info('[NATS] subscribed to subject', subject);
|
|
181
|
+
let received = 0;
|
|
182
|
+
for await (const msg of sub) {
|
|
183
|
+
received += 1;
|
|
184
|
+
try {
|
|
185
|
+
decodeMessage(
|
|
186
|
+
subject,
|
|
187
|
+
received,
|
|
188
|
+
msg.json<Record<string, unknown>>(),
|
|
189
|
+
onMessage
|
|
190
|
+
);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error('[NATS] message decode error', err);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
console.info(
|
|
196
|
+
`[NATS] subscription on ${subject} closed (received ${received} messages)`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function consumeJetStream(
|
|
201
|
+
nc: NatsConnection,
|
|
202
|
+
config: Pick<NatsConfig, 'stream' | 'consumer'>,
|
|
203
|
+
subject: string,
|
|
204
|
+
mode: JetStreamConsumeMode,
|
|
205
|
+
onMessage: (event: NatsSessionEvent) => void,
|
|
206
|
+
setConsumerMessages: (messages: ConsumerMessages) => void
|
|
207
|
+
) {
|
|
208
|
+
const js = jetstream(nc);
|
|
209
|
+
const consumer = config.consumer
|
|
210
|
+
? await js.consumers.get(config.stream!, config.consumer)
|
|
211
|
+
: await js.consumers.get(config.stream!, {
|
|
212
|
+
filter_subjects: [subject],
|
|
213
|
+
deliver_policy:
|
|
214
|
+
mode === 'catch-up'
|
|
215
|
+
? DeliverPolicy.LastPerSubject
|
|
216
|
+
: DeliverPolicy.New,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
console.info(
|
|
220
|
+
'[NATS] JetStream consumer ready',
|
|
221
|
+
config.consumer
|
|
222
|
+
? { stream: config.stream, consumer: config.consumer, subject, mode }
|
|
223
|
+
: { stream: config.stream, subject, mode }
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const messages = await consumer.consume();
|
|
227
|
+
setConsumerMessages(messages);
|
|
228
|
+
let received = 0;
|
|
229
|
+
for await (const msg of messages) {
|
|
230
|
+
received += 1;
|
|
231
|
+
try {
|
|
232
|
+
decodeMessage(subject, received, msg.json(), onMessage);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
console.error('[NATS] message decode error', err);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
console.info(
|
|
238
|
+
`[NATS] JetStream consumer on ${subject} closed (received ${received} messages)`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function closeNatsSession(
|
|
243
|
+
consumerMessagesRef: MutableRefObject<ConsumerMessages | null>,
|
|
244
|
+
connRef: MutableRefObject<NatsConnection | null>
|
|
245
|
+
) {
|
|
246
|
+
consumerMessagesRef.current?.stop();
|
|
247
|
+
consumerMessagesRef.current = null;
|
|
248
|
+
connRef.current?.close();
|
|
249
|
+
connRef.current = null;
|
|
250
|
+
}
|
|
251
|
+
|
|
146
252
|
/**
|
|
147
253
|
* Open a NATS WebSocket connection, subscribe to the session subject and
|
|
148
254
|
* forward decoded events to `onMessage`.
|
|
149
255
|
*
|
|
150
256
|
* `onMessage` is kept in a ref so callers do not need to memoize it: the
|
|
151
|
-
* connection is only (re)opened when `sessionId`, `
|
|
257
|
+
* connection is only (re)opened when `sessionId`, `config` or `resumeGeneration`
|
|
152
258
|
* change.
|
|
153
259
|
*
|
|
154
260
|
* @param sessionId Current session UUID (subscription is skipped when falsy).
|
|
155
|
-
* @param
|
|
156
|
-
* @param natsToken Bearer token for authentication.
|
|
261
|
+
* @param config Connection config from `/api/nats` (url + token required).
|
|
157
262
|
* @param onMessage Callback invoked for each decoded event.
|
|
158
263
|
*/
|
|
159
264
|
export function useNatsSession(
|
|
160
265
|
sessionId: string | undefined,
|
|
161
|
-
|
|
162
|
-
natsToken: string | undefined,
|
|
266
|
+
config: NatsConfig | undefined,
|
|
163
267
|
onMessage: (event: NatsSessionEvent) => void
|
|
164
268
|
) {
|
|
165
269
|
const connRef = useRef<NatsConnection | null>(null);
|
|
270
|
+
const consumerMessagesRef = useRef<ConsumerMessages | null>(null);
|
|
166
271
|
const onMessageRef = useRef(onMessage);
|
|
272
|
+
const wasHiddenRef = useRef(false);
|
|
273
|
+
const [resumeGeneration, setResumeGeneration] = useState(0);
|
|
167
274
|
|
|
168
275
|
// Keep the latest callback without retriggering the connection effect.
|
|
169
276
|
useEffect(() => {
|
|
170
277
|
onMessageRef.current = onMessage;
|
|
171
278
|
}, [onMessage]);
|
|
172
279
|
|
|
280
|
+
// Reconnect when the tab returns from background or the network comes back.
|
|
173
281
|
useEffect(() => {
|
|
174
|
-
if (!sessionId || !
|
|
282
|
+
if (!sessionId || !config?.stream) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const requestResume = () => {
|
|
287
|
+
if (document.visibilityState !== 'visible') {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
console.info('[NATS] resuming JetStream after visibility/network change');
|
|
291
|
+
setResumeGeneration(generation => generation + 1);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const onVisibilityChange = () => {
|
|
295
|
+
if (document.visibilityState === 'hidden') {
|
|
296
|
+
wasHiddenRef.current = true;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (wasHiddenRef.current) {
|
|
300
|
+
wasHiddenRef.current = false;
|
|
301
|
+
requestResume();
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const onPageShow = (event: PageTransitionEvent) => {
|
|
306
|
+
if (event.persisted) {
|
|
307
|
+
requestResume();
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
312
|
+
window.addEventListener('online', requestResume);
|
|
313
|
+
window.addEventListener('pageshow', onPageShow);
|
|
314
|
+
|
|
315
|
+
return () => {
|
|
316
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
317
|
+
window.removeEventListener('online', requestResume);
|
|
318
|
+
window.removeEventListener('pageshow', onPageShow);
|
|
319
|
+
};
|
|
320
|
+
}, [sessionId, config?.stream]);
|
|
321
|
+
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (!sessionId || !config?.url || !config.token) {
|
|
175
324
|
console.debug(
|
|
176
325
|
'[NATS] subscription skipped (missing sessionId/url/token)',
|
|
177
|
-
{
|
|
326
|
+
{
|
|
327
|
+
sessionId,
|
|
328
|
+
hasUrl: !!config?.url,
|
|
329
|
+
hasToken: !!config?.token,
|
|
330
|
+
}
|
|
178
331
|
);
|
|
179
332
|
return;
|
|
180
333
|
}
|
|
334
|
+
|
|
181
335
|
let closed = false;
|
|
182
|
-
const subject = sessionId
|
|
336
|
+
const subject = config.subject ?? `sessions.${sessionId}`;
|
|
337
|
+
const useJetStream = !!config.stream;
|
|
338
|
+
const jetStreamMode: JetStreamConsumeMode =
|
|
339
|
+
useJetStream && resumeGeneration > 0 ? 'catch-up' : 'live';
|
|
183
340
|
|
|
184
341
|
(async () => {
|
|
185
|
-
console.info(
|
|
342
|
+
console.info(
|
|
343
|
+
'[NATS] connecting to',
|
|
344
|
+
config.url,
|
|
345
|
+
'for session',
|
|
346
|
+
sessionId,
|
|
347
|
+
useJetStream ? `(JetStream/${jetStreamMode})` : '(core)'
|
|
348
|
+
);
|
|
186
349
|
const nc = await wsconnect({
|
|
187
|
-
servers: [
|
|
188
|
-
token:
|
|
350
|
+
servers: [config.url],
|
|
351
|
+
token: config.token,
|
|
189
352
|
});
|
|
190
353
|
if (closed) {
|
|
191
354
|
console.debug(
|
|
@@ -197,34 +360,35 @@ export function useNatsSession(
|
|
|
197
360
|
connRef.current = nc;
|
|
198
361
|
console.info('[NATS] connected to', nc.getServer());
|
|
199
362
|
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
363
|
+
const dispatch = (event: NatsSessionEvent) => {
|
|
364
|
+
onMessageRef.current(event);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
if (useJetStream) {
|
|
368
|
+
await consumeJetStream(
|
|
369
|
+
nc,
|
|
370
|
+
config,
|
|
371
|
+
subject,
|
|
372
|
+
jetStreamMode,
|
|
373
|
+
dispatch,
|
|
374
|
+
messages => {
|
|
375
|
+
consumerMessagesRef.current = messages;
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
await consumeCoreSubscription(nc, subject, dispatch);
|
|
217
380
|
}
|
|
218
|
-
console.info(
|
|
219
|
-
`[NATS] subscription on ${subject} closed (received ${received} messages)`
|
|
220
|
-
);
|
|
221
381
|
})().catch(err => console.error('[NATS] connection error', err));
|
|
222
382
|
|
|
223
383
|
return () => {
|
|
224
384
|
console.info('[NATS] cleanup: closing connection for subject', subject);
|
|
225
385
|
closed = true;
|
|
226
|
-
connRef
|
|
227
|
-
connRef.current = null;
|
|
386
|
+
closeNatsSession(consumerMessagesRef, connRef);
|
|
228
387
|
};
|
|
229
|
-
}, [sessionId,
|
|
388
|
+
}, [sessionId, config, resumeGeneration]);
|
|
389
|
+
|
|
390
|
+
// Reset catch-up generation when the active session changes.
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
setResumeGeneration(0);
|
|
393
|
+
}, [sessionId]);
|
|
230
394
|
}
|
package/src/index.stories.tsx
CHANGED
|
@@ -202,3 +202,19 @@ WithLocalNats.args = {
|
|
|
202
202
|
spokenLang: 'IT',
|
|
203
203
|
integrationID: 'ee1c3d98-7819-4506-ba28-818e79ba86cb',
|
|
204
204
|
};
|
|
205
|
+
|
|
206
|
+
export const WithFunctionalities = Template.bind({});
|
|
207
|
+
WithFunctionalities.args = {
|
|
208
|
+
memoriName: 'test324',
|
|
209
|
+
ownerUserName: 'andrea.patini',
|
|
210
|
+
memoriID: 'd661a9ca-e907-4396-a986-5095ccd582d6',
|
|
211
|
+
ownerUserID: '69fcc557-9cb6-4e5e-b8ab-140cff975492',
|
|
212
|
+
tenantID: 'localhost:3000',
|
|
213
|
+
engineURL: 'http://localhost:7778/memori/v2',
|
|
214
|
+
apiURL: 'http://localhost:7778/api/v2',
|
|
215
|
+
baseURL: 'http://localhost:3000',
|
|
216
|
+
layout: 'FULLPAGE',
|
|
217
|
+
uiLang: 'IT',
|
|
218
|
+
spokenLang: 'IT',
|
|
219
|
+
integrationID: 'ee1c3d98-7819-4506-ba28-818e79ba86cb',
|
|
220
|
+
};
|
package/src/locales/de.json
CHANGED
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"errorFetchingSession": "Fehler beim Laden der Sitzung",
|
|
76
76
|
"errorGettingReferralURL": "Fehler beim Laden des Referrals",
|
|
77
77
|
"errorReopeningSession": "Fehler beim erneuten Öffnen der Sitzung",
|
|
78
|
+
"sessionExpiredReopening": "Sitzung abgelaufen, Sitzung wird neu geöffnet",
|
|
78
79
|
"ageVerification": "Altersüberprüfung",
|
|
79
80
|
"ageVerificationText": "Um mit diesem Zwilling interagieren zu können, müssen Sie mindestens sein {{minAge}} Jahre alt.",
|
|
80
81
|
"nsfw": "NSFW: Dieser Agent enthält Inhalte für Erwachsene",
|
package/src/locales/en.json
CHANGED
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"errorFetchingSession": "Error during session loading",
|
|
78
78
|
"errorGettingReferralURL": "Error during referral loading",
|
|
79
79
|
"errorReopeningSession": "Error during session reopening",
|
|
80
|
+
"sessionExpiredReopening": "Session expired, reopening session",
|
|
80
81
|
"ageVerification": "Age verification",
|
|
81
82
|
"ageVerificationText": "To interact with this agent, you must be at least {{minAge}} years old.",
|
|
82
83
|
"nsfw": "NSFW: This agent contains adult contents",
|
package/src/locales/es.json
CHANGED
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"errorFetchingSession": "Error durante el cargamento de la sesión",
|
|
76
76
|
"errorGettingReferralURL": "Error durante el cargamento del référent",
|
|
77
77
|
"errorReopeningSession": "Error durante el re-abrir la sesión",
|
|
78
|
+
"sessionExpiredReopening": "Sesión expirada, reabriendo sesión",
|
|
78
79
|
"ageVerification": "Verificación de edad",
|
|
79
80
|
"ageVerificationText": "Para interactuar con este Gemelo, debes tener al menos {{minAge}} años.",
|
|
80
81
|
"nsfw": "NSFW: Este gemelo contiene contenido para adultos",
|
package/src/locales/fr.json
CHANGED
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"errorFetchingSession": "Erreur lors du chargement de la session",
|
|
75
75
|
"errorGettingReferralURL": "Erreur lors du chargement du référent",
|
|
76
76
|
"errorReopeningSession": "Erreur lors de la re-ouverture de la session",
|
|
77
|
+
"sessionExpiredReopening": "Session expirée, réouverture en cours",
|
|
77
78
|
"ageVerification": "Vérification de l'âge",
|
|
78
79
|
"ageVerificationText": "Pour interagir avec ce Agent, vous devez être au minimum {{minAge}} ans.",
|
|
79
80
|
"nsfw": "NSFW : Ce jumeau contient du contenu pour adultes",
|
package/src/locales/it.json
CHANGED
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"errorFetchingSession": "Errore durante il caricamento della sessione",
|
|
79
79
|
"errorGettingReferralURL": "Errore durante il caricamento del riferimento",
|
|
80
80
|
"errorReopeningSession": "Errore durante il riapertura della sessione",
|
|
81
|
+
"sessionExpiredReopening": "Sessione scaduta, riapertura in corso",
|
|
81
82
|
"ageVerification": "Verifica dell'età",
|
|
82
83
|
"ageVerificationText": "Per interagire con questo agente, devi aver almeno {{minAge}} anni.",
|
|
83
84
|
"nsfw": "NSFW: Questo agente contiene contenuti per adulti",
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// This file is auto-generated. Do not edit manually.
|
|
2
|
-
export const version = '8.
|
|
2
|
+
export const version = '8.41.1';
|