@revealui/sync 0.0.0-canary-20260409021642
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 +22 -0
- package/README.md +201 -0
- package/dist/collab/agent-client-factory.d.ts +16 -0
- package/dist/collab/agent-client-factory.d.ts.map +1 -0
- package/dist/collab/agent-client-factory.js +24 -0
- package/dist/collab/agent-client.d.ts +56 -0
- package/dist/collab/agent-client.d.ts.map +1 -0
- package/dist/collab/agent-client.js +262 -0
- package/dist/collab/index.d.ts +8 -0
- package/dist/collab/index.d.ts.map +1 -0
- package/dist/collab/index.js +4 -0
- package/dist/collab/protocol-constants.d.ts +5 -0
- package/dist/collab/protocol-constants.d.ts.map +1 -0
- package/dist/collab/protocol-constants.js +4 -0
- package/dist/collab/server-index.d.ts +6 -0
- package/dist/collab/server-index.d.ts.map +1 -0
- package/dist/collab/server-index.js +3 -0
- package/dist/collab/use-collab-document.d.ts +8 -0
- package/dist/collab/use-collab-document.d.ts.map +1 -0
- package/dist/collab/use-collab-document.js +49 -0
- package/dist/collab/use-collaboration.d.ts +26 -0
- package/dist/collab/use-collaboration.d.ts.map +1 -0
- package/dist/collab/use-collaboration.js +65 -0
- package/dist/collab/yjs-websocket-provider.d.ts +36 -0
- package/dist/collab/yjs-websocket-provider.d.ts.map +1 -0
- package/dist/collab/yjs-websocket-provider.js +172 -0
- package/dist/components/SyncStatusIndicator.d.ts +17 -0
- package/dist/components/SyncStatusIndicator.d.ts.map +1 -0
- package/dist/components/SyncStatusIndicator.js +65 -0
- package/dist/fetch-with-timeout.d.ts +7 -0
- package/dist/fetch-with-timeout.d.ts.map +1 -0
- package/dist/fetch-with-timeout.js +27 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/useAgentContexts.d.ts +29 -0
- package/dist/hooks/useAgentContexts.d.ts.map +1 -0
- package/dist/hooks/useAgentContexts.js +22 -0
- package/dist/hooks/useAgentMemory.d.ts +34 -0
- package/dist/hooks/useAgentMemory.d.ts.map +1 -0
- package/dist/hooks/useAgentMemory.js +37 -0
- package/dist/hooks/useConversations.d.ts +31 -0
- package/dist/hooks/useConversations.d.ts.map +1 -0
- package/dist/hooks/useConversations.js +26 -0
- package/dist/hooks/useCoordinationSessions.d.ts +35 -0
- package/dist/hooks/useCoordinationSessions.d.ts.map +1 -0
- package/dist/hooks/useCoordinationSessions.js +22 -0
- package/dist/hooks/useCoordinationWorkItems.d.ts +41 -0
- package/dist/hooks/useCoordinationWorkItems.d.ts.map +1 -0
- package/dist/hooks/useCoordinationWorkItems.js +22 -0
- package/dist/hooks/useOfflineCache.d.ts +32 -0
- package/dist/hooks/useOfflineCache.d.ts.map +1 -0
- package/dist/hooks/useOfflineCache.js +129 -0
- package/dist/hooks/useOnlineStatus.d.ts +21 -0
- package/dist/hooks/useOnlineStatus.d.ts.map +1 -0
- package/dist/hooks/useOnlineStatus.js +74 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/mutations.d.ts +14 -0
- package/dist/mutations.d.ts.map +1 -0
- package/dist/mutations.js +53 -0
- package/dist/offline-queue.d.ts +55 -0
- package/dist/offline-queue.d.ts.map +1 -0
- package/dist/offline-queue.js +126 -0
- package/dist/provider/index.d.ts +35 -0
- package/dist/provider/index.d.ts.map +1 -0
- package/dist/provider/index.js +29 -0
- package/dist/shape-utils.d.ts +11 -0
- package/dist/shape-utils.d.ts.map +1 -0
- package/dist/shape-utils.js +14 -0
- package/dist/test-setup.d.ts +2 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useShape } from '@electric-sql/react';
|
|
2
|
+
import { fetchWithTimeout } from '../fetch-with-timeout.js';
|
|
3
|
+
import { useElectricConfig } from '../provider/index.js';
|
|
4
|
+
// UUID v4 pattern — only format accepted as a yjs_documents PK
|
|
5
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
|
+
export function useCollabDocument(documentId) {
|
|
7
|
+
const { proxyBaseUrl } = useElectricConfig();
|
|
8
|
+
// Validate before interpolating into the WHERE clause. yjs_documents PKs are
|
|
9
|
+
// always UUIDs — reject anything else so no untrusted string enters the query.
|
|
10
|
+
const isValidId = UUID_RE.test(documentId);
|
|
11
|
+
// Hook must always be called (Rules of Hooks). Pass an impossible WHERE when
|
|
12
|
+
// the ID is invalid so the shape returns no rows but the hook still runs.
|
|
13
|
+
const { data, isLoading, error } = useShape({
|
|
14
|
+
url: `${proxyBaseUrl}/api/shapes/yjs-documents`,
|
|
15
|
+
params: {
|
|
16
|
+
document_id: isValidId ? documentId : '__invalid__',
|
|
17
|
+
},
|
|
18
|
+
fetchClient: fetchWithTimeout,
|
|
19
|
+
});
|
|
20
|
+
if (!isValidId) {
|
|
21
|
+
return {
|
|
22
|
+
initialState: null,
|
|
23
|
+
connectedClients: 0,
|
|
24
|
+
isLoading: false,
|
|
25
|
+
error: new Error('Invalid documentId: must be a UUID'),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
let initialState = null;
|
|
29
|
+
let connectedClients = 0;
|
|
30
|
+
if (data && data.length > 0) {
|
|
31
|
+
const row = data[0];
|
|
32
|
+
const state = row.state;
|
|
33
|
+
if (state) {
|
|
34
|
+
const binary = atob(state);
|
|
35
|
+
const bytes = new Uint8Array(binary.length);
|
|
36
|
+
for (let i = 0; i < binary.length; i++) {
|
|
37
|
+
bytes[i] = binary.charCodeAt(i);
|
|
38
|
+
}
|
|
39
|
+
initialState = bytes;
|
|
40
|
+
}
|
|
41
|
+
connectedClients = row.connected_clients ?? 0;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
initialState,
|
|
45
|
+
connectedClients,
|
|
46
|
+
isLoading,
|
|
47
|
+
error: error ? new Error(String(error)) : null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
import type { UserPresence } from './yjs-websocket-provider.js';
|
|
3
|
+
import { CollabProvider } from './yjs-websocket-provider.js';
|
|
4
|
+
export interface CollaborationIdentity {
|
|
5
|
+
name: string;
|
|
6
|
+
color: string;
|
|
7
|
+
type?: 'human' | 'agent';
|
|
8
|
+
agentModel?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface UseCollaborationOptions {
|
|
11
|
+
documentId: string;
|
|
12
|
+
serverUrl: string;
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
initialState?: Uint8Array | null;
|
|
15
|
+
identity?: CollaborationIdentity;
|
|
16
|
+
}
|
|
17
|
+
export interface UseCollaborationResult {
|
|
18
|
+
doc: Y.Doc | null;
|
|
19
|
+
provider: CollabProvider | null;
|
|
20
|
+
synced: boolean;
|
|
21
|
+
status: string;
|
|
22
|
+
error: Error | null;
|
|
23
|
+
connectedUsers: Map<number, UserPresence>;
|
|
24
|
+
}
|
|
25
|
+
export declare function useCollaboration({ documentId, serverUrl, enabled, initialState, identity, }: UseCollaborationOptions): UseCollaborationResult;
|
|
26
|
+
//# sourceMappingURL=use-collaboration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-collaboration.d.ts","sourceRoot":"","sources":["../../src/collab/use-collaboration.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE7D,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,qBAAqB,CAAC;CAClC;AAED,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC;IAClB,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;IAChC,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CAC3C;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,SAAS,EACT,OAAc,EACd,YAAmB,EACnB,QAAQ,GACT,EAAE,uBAAuB,GAAG,sBAAsB,CAqElD"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import * as Y from 'yjs';
|
|
3
|
+
import { CollabProvider } from './yjs-websocket-provider.js';
|
|
4
|
+
export function useCollaboration({ documentId, serverUrl, enabled = true, initialState = null, identity, }) {
|
|
5
|
+
const [synced, setSynced] = useState(false);
|
|
6
|
+
const [status, setStatus] = useState('disconnected');
|
|
7
|
+
const [error, setError] = useState(null);
|
|
8
|
+
const [connectedUsers, setConnectedUsers] = useState(new Map());
|
|
9
|
+
const docRef = useRef(null);
|
|
10
|
+
const providerRef = useRef(null);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!(enabled && documentId))
|
|
13
|
+
return;
|
|
14
|
+
const doc = new Y.Doc();
|
|
15
|
+
docRef.current = doc;
|
|
16
|
+
try {
|
|
17
|
+
const provider = new CollabProvider(serverUrl, documentId, doc, { initialState });
|
|
18
|
+
providerRef.current = provider;
|
|
19
|
+
// Set local identity for awareness
|
|
20
|
+
if (identity) {
|
|
21
|
+
provider.setLocalIdentity({
|
|
22
|
+
name: identity.name,
|
|
23
|
+
color: identity.color,
|
|
24
|
+
type: identity.type ?? 'human',
|
|
25
|
+
...(identity.agentModel ? { agentModel: identity.agentModel } : {}),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
provider.on('sync', (isSynced) => {
|
|
29
|
+
setSynced(isSynced);
|
|
30
|
+
});
|
|
31
|
+
provider.on('status', (event) => {
|
|
32
|
+
const statusEvent = event;
|
|
33
|
+
setStatus(statusEvent.status);
|
|
34
|
+
});
|
|
35
|
+
provider.on('awareness', (users) => {
|
|
36
|
+
setConnectedUsers(new Map(users));
|
|
37
|
+
});
|
|
38
|
+
provider.connect();
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
42
|
+
}
|
|
43
|
+
return () => {
|
|
44
|
+
if (providerRef.current) {
|
|
45
|
+
providerRef.current.destroy();
|
|
46
|
+
providerRef.current = null;
|
|
47
|
+
}
|
|
48
|
+
if (docRef.current) {
|
|
49
|
+
docRef.current.destroy();
|
|
50
|
+
docRef.current = null;
|
|
51
|
+
}
|
|
52
|
+
setSynced(false);
|
|
53
|
+
setStatus('disconnected');
|
|
54
|
+
setConnectedUsers(new Map());
|
|
55
|
+
};
|
|
56
|
+
}, [documentId, serverUrl, enabled, initialState, identity]);
|
|
57
|
+
return {
|
|
58
|
+
doc: docRef.current,
|
|
59
|
+
provider: providerRef.current,
|
|
60
|
+
synced,
|
|
61
|
+
status,
|
|
62
|
+
error,
|
|
63
|
+
connectedUsers,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Observable } from 'lib0/observable';
|
|
2
|
+
import * as awarenessProtocol from 'y-protocols/awareness';
|
|
3
|
+
import * as Y from 'yjs';
|
|
4
|
+
export interface UserPresence {
|
|
5
|
+
name: string;
|
|
6
|
+
color: string;
|
|
7
|
+
type: 'human' | 'agent';
|
|
8
|
+
agentModel?: string;
|
|
9
|
+
cursor?: {
|
|
10
|
+
index: number;
|
|
11
|
+
length: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare class CollabProvider extends Observable<string> {
|
|
15
|
+
doc: Y.Doc;
|
|
16
|
+
awareness: awarenessProtocol.Awareness;
|
|
17
|
+
private serverUrl;
|
|
18
|
+
private documentId;
|
|
19
|
+
private ws;
|
|
20
|
+
private synced;
|
|
21
|
+
private reconnectAttempts;
|
|
22
|
+
private reconnectTimer;
|
|
23
|
+
private updateHandler;
|
|
24
|
+
private awarenessUpdateHandler;
|
|
25
|
+
constructor(serverUrl: string, documentId: string, doc: Y.Doc, options?: {
|
|
26
|
+
initialState?: Uint8Array | null;
|
|
27
|
+
});
|
|
28
|
+
setLocalIdentity(identity: UserPresence): void;
|
|
29
|
+
getConnectedUsers(): Map<number, UserPresence>;
|
|
30
|
+
connect(): void;
|
|
31
|
+
disconnect(): void;
|
|
32
|
+
private scheduleReconnect;
|
|
33
|
+
private cancelReconnect;
|
|
34
|
+
destroy(): void;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=yjs-websocket-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"yjs-websocket-provider.d.ts","sourceRoot":"","sources":["../../src/collab/yjs-websocket-provider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,iBAAiB,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAOzB,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5C;AAED,qBAAa,cAAe,SAAQ,UAAU,CAAC,MAAM,CAAC;IACpD,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;IACX,SAAS,EAAE,iBAAiB,CAAC,SAAS,CAAC;IACvC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,aAAa,CAAgE;IACrF,OAAO,CAAC,sBAAsB,CAGpB;gBAGR,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,CAAC,CAAC,GAAG,EACV,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,UAAU,GAAG,IAAI,CAAA;KAAE;IA2BhD,gBAAgB,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI;IAI9C,iBAAiB,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC;IAU9C,OAAO,IAAI,IAAI;IAuFf,UAAU,IAAI,IAAI;IAmBlB,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,eAAe;IAQvB,OAAO,IAAI,IAAI;CAMhB"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import * as decoding from 'lib0/decoding';
|
|
2
|
+
import * as encoding from 'lib0/encoding';
|
|
3
|
+
import { Observable } from 'lib0/observable';
|
|
4
|
+
import * as awarenessProtocol from 'y-protocols/awareness';
|
|
5
|
+
import * as syncProtocol from 'y-protocols/sync';
|
|
6
|
+
import * as Y from 'yjs';
|
|
7
|
+
import { MESSAGE_AWARENESS, MESSAGE_SYNC } from './protocol-constants.js';
|
|
8
|
+
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
9
|
+
const BASE_RECONNECT_DELAY = 1000;
|
|
10
|
+
const RECONNECT_MULTIPLIER = 1.5;
|
|
11
|
+
export class CollabProvider extends Observable {
|
|
12
|
+
doc;
|
|
13
|
+
awareness;
|
|
14
|
+
serverUrl;
|
|
15
|
+
documentId;
|
|
16
|
+
ws = null;
|
|
17
|
+
synced = false;
|
|
18
|
+
reconnectAttempts = 0;
|
|
19
|
+
reconnectTimer = null;
|
|
20
|
+
updateHandler = null;
|
|
21
|
+
awarenessUpdateHandler;
|
|
22
|
+
constructor(serverUrl, documentId, doc, options) {
|
|
23
|
+
super();
|
|
24
|
+
this.serverUrl = serverUrl;
|
|
25
|
+
this.documentId = documentId;
|
|
26
|
+
this.doc = doc;
|
|
27
|
+
this.awareness = new awarenessProtocol.Awareness(doc);
|
|
28
|
+
if (options?.initialState) {
|
|
29
|
+
Y.applyUpdate(doc, options.initialState);
|
|
30
|
+
}
|
|
31
|
+
// Send awareness updates over WebSocket
|
|
32
|
+
this.awarenessUpdateHandler = ({ added, updated, removed }, origin) => {
|
|
33
|
+
if (origin === 'remote')
|
|
34
|
+
return;
|
|
35
|
+
const changedClients = added.concat(updated, removed);
|
|
36
|
+
const update = awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients);
|
|
37
|
+
const encoder = encoding.createEncoder();
|
|
38
|
+
encoding.writeVarUint(encoder, MESSAGE_AWARENESS);
|
|
39
|
+
encoding.writeVarUint8Array(encoder, update);
|
|
40
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
41
|
+
this.ws.send(encoding.toUint8Array(encoder));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
this.awareness.on('update', this.awarenessUpdateHandler);
|
|
45
|
+
}
|
|
46
|
+
setLocalIdentity(identity) {
|
|
47
|
+
this.awareness.setLocalState(identity);
|
|
48
|
+
}
|
|
49
|
+
getConnectedUsers() {
|
|
50
|
+
const users = new Map();
|
|
51
|
+
for (const [clientId, state] of this.awareness.getStates()) {
|
|
52
|
+
if (state && typeof state === 'object') {
|
|
53
|
+
users.set(clientId, state);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return users;
|
|
57
|
+
}
|
|
58
|
+
connect() {
|
|
59
|
+
if (this.ws)
|
|
60
|
+
return;
|
|
61
|
+
const url = `${this.serverUrl}/ws/collab/${this.documentId}`;
|
|
62
|
+
this.ws = new WebSocket(url);
|
|
63
|
+
this.ws.binaryType = 'arraybuffer';
|
|
64
|
+
this.emit('status', [{ status: 'connecting' }]);
|
|
65
|
+
this.ws.onopen = () => {
|
|
66
|
+
this.reconnectAttempts = 0;
|
|
67
|
+
this.emit('status', [{ status: 'connected' }]);
|
|
68
|
+
const encoder = encoding.createEncoder();
|
|
69
|
+
encoding.writeVarUint(encoder, MESSAGE_SYNC);
|
|
70
|
+
syncProtocol.writeSyncStep1(encoder, this.doc);
|
|
71
|
+
this.ws?.send(encoding.toUint8Array(encoder));
|
|
72
|
+
// Send local awareness state to server
|
|
73
|
+
const localState = this.awareness.getLocalState();
|
|
74
|
+
if (localState) {
|
|
75
|
+
const awarenessUpdate = awarenessProtocol.encodeAwarenessUpdate(this.awareness, [
|
|
76
|
+
this.doc.clientID,
|
|
77
|
+
]);
|
|
78
|
+
const awarenessEncoder = encoding.createEncoder();
|
|
79
|
+
encoding.writeVarUint(awarenessEncoder, MESSAGE_AWARENESS);
|
|
80
|
+
encoding.writeVarUint8Array(awarenessEncoder, awarenessUpdate);
|
|
81
|
+
this.ws?.send(encoding.toUint8Array(awarenessEncoder));
|
|
82
|
+
}
|
|
83
|
+
this.updateHandler = (update, origin) => {
|
|
84
|
+
if (origin === this)
|
|
85
|
+
return;
|
|
86
|
+
const updateEncoder = encoding.createEncoder();
|
|
87
|
+
encoding.writeVarUint(updateEncoder, MESSAGE_SYNC);
|
|
88
|
+
syncProtocol.writeUpdate(updateEncoder, update);
|
|
89
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
90
|
+
this.ws.send(encoding.toUint8Array(updateEncoder));
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
this.doc.on('update', this.updateHandler);
|
|
94
|
+
};
|
|
95
|
+
this.ws.onmessage = (event) => {
|
|
96
|
+
const data = new Uint8Array(event.data);
|
|
97
|
+
const decoder = decoding.createDecoder(data);
|
|
98
|
+
const messageType = decoding.readVarUint(decoder);
|
|
99
|
+
if (messageType === MESSAGE_SYNC) {
|
|
100
|
+
const responseEncoder = encoding.createEncoder();
|
|
101
|
+
encoding.writeVarUint(responseEncoder, MESSAGE_SYNC);
|
|
102
|
+
const syncMessageType = syncProtocol.readSyncMessage(decoder, responseEncoder, this.doc, this);
|
|
103
|
+
if (encoding.length(responseEncoder) > 1) {
|
|
104
|
+
this.ws?.send(encoding.toUint8Array(responseEncoder));
|
|
105
|
+
}
|
|
106
|
+
if (syncMessageType === 1 && !this.synced)
|
|
107
|
+
this.synced = true;
|
|
108
|
+
this.emit('sync', [true]);
|
|
109
|
+
}
|
|
110
|
+
else if (messageType === MESSAGE_AWARENESS) {
|
|
111
|
+
const update = decoding.readVarUint8Array(decoder);
|
|
112
|
+
awarenessProtocol.applyAwarenessUpdate(this.awareness, update, 'remote');
|
|
113
|
+
this.emit('awareness', [this.getConnectedUsers()]);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
this.ws.onclose = () => {
|
|
117
|
+
if (this.updateHandler) {
|
|
118
|
+
this.doc.off('update', this.updateHandler);
|
|
119
|
+
this.updateHandler = null;
|
|
120
|
+
}
|
|
121
|
+
this.ws = null;
|
|
122
|
+
this.synced = false;
|
|
123
|
+
this.emit('sync', [false]);
|
|
124
|
+
this.emit('status', [{ status: 'disconnected' }]);
|
|
125
|
+
this.scheduleReconnect();
|
|
126
|
+
};
|
|
127
|
+
this.ws.onerror = () => {
|
|
128
|
+
this.ws?.close();
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
disconnect() {
|
|
132
|
+
this.cancelReconnect();
|
|
133
|
+
if (this.updateHandler) {
|
|
134
|
+
this.doc.off('update', this.updateHandler);
|
|
135
|
+
this.updateHandler = null;
|
|
136
|
+
}
|
|
137
|
+
// Broadcast awareness removal before disconnecting
|
|
138
|
+
awarenessProtocol.removeAwarenessStates(this.awareness, [this.doc.clientID], 'local');
|
|
139
|
+
if (this.ws) {
|
|
140
|
+
this.ws.onclose = null;
|
|
141
|
+
this.ws.close();
|
|
142
|
+
this.ws = null;
|
|
143
|
+
}
|
|
144
|
+
this.synced = false;
|
|
145
|
+
this.emit('status', [{ status: 'disconnected' }]);
|
|
146
|
+
}
|
|
147
|
+
scheduleReconnect() {
|
|
148
|
+
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
149
|
+
this.emit('status', [{ status: 'failed' }]);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const delay = BASE_RECONNECT_DELAY * RECONNECT_MULTIPLIER ** this.reconnectAttempts;
|
|
153
|
+
this.reconnectAttempts++;
|
|
154
|
+
this.reconnectTimer = setTimeout(() => {
|
|
155
|
+
this.reconnectTimer = null;
|
|
156
|
+
this.connect();
|
|
157
|
+
}, delay);
|
|
158
|
+
}
|
|
159
|
+
cancelReconnect() {
|
|
160
|
+
if (this.reconnectTimer) {
|
|
161
|
+
clearTimeout(this.reconnectTimer);
|
|
162
|
+
this.reconnectTimer = null;
|
|
163
|
+
}
|
|
164
|
+
this.reconnectAttempts = 0;
|
|
165
|
+
}
|
|
166
|
+
destroy() {
|
|
167
|
+
this.awareness.off('update', this.awarenessUpdateHandler);
|
|
168
|
+
this.disconnect();
|
|
169
|
+
this.awareness.destroy();
|
|
170
|
+
super.destroy();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface SyncStatusIndicatorProps {
|
|
2
|
+
/** Optional CSS class name for positioning or layout overrides. */
|
|
3
|
+
className?: string;
|
|
4
|
+
/** Whether data is currently being synced. */
|
|
5
|
+
isSyncing?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Visual indicator for sync / connectivity status.
|
|
9
|
+
*
|
|
10
|
+
* - **Online + synced**: green dot
|
|
11
|
+
* - **Online + syncing**: pulsing yellow dot
|
|
12
|
+
* - **Offline**: red dot with "Offline" label
|
|
13
|
+
* - **Recently reconnected**: green dot with "Synced" label (fades after 3 s)
|
|
14
|
+
*/
|
|
15
|
+
export declare function SyncStatusIndicator(props: SyncStatusIndicatorProps): React.ReactNode;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=SyncStatusIndicator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SyncStatusIndicator.d.ts","sourceRoot":"","sources":["../../src/components/SyncStatusIndicator.tsx"],"names":[],"mappings":"AAQA,UAAU,wBAAwB;IAChC,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,GAAG,KAAK,CAAC,SAAS,CAgEpF"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useOnlineStatus } from '../hooks/useOnlineStatus.js';
|
|
5
|
+
/** Duration in ms before the "Synced" label fades away. */
|
|
6
|
+
const SYNCED_LABEL_DURATION_MS = 3_000;
|
|
7
|
+
/**
|
|
8
|
+
* Visual indicator for sync / connectivity status.
|
|
9
|
+
*
|
|
10
|
+
* - **Online + synced**: green dot
|
|
11
|
+
* - **Online + syncing**: pulsing yellow dot
|
|
12
|
+
* - **Offline**: red dot with "Offline" label
|
|
13
|
+
* - **Recently reconnected**: green dot with "Synced" label (fades after 3 s)
|
|
14
|
+
*/
|
|
15
|
+
export function SyncStatusIndicator(props) {
|
|
16
|
+
const { className, isSyncing = false } = props;
|
|
17
|
+
const { isOnline, wasOffline } = useOnlineStatus();
|
|
18
|
+
// Show "Synced" label for 3 s after reconnection.
|
|
19
|
+
const [showSyncedLabel, setShowSyncedLabel] = useState(false);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!wasOffline) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
setShowSyncedLabel(true);
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
setShowSyncedLabel(false);
|
|
27
|
+
}, SYNCED_LABEL_DURATION_MS);
|
|
28
|
+
return () => clearTimeout(timer);
|
|
29
|
+
}, [wasOffline]);
|
|
30
|
+
// Determine dot color and label.
|
|
31
|
+
let dotColor;
|
|
32
|
+
let label = null;
|
|
33
|
+
let pulse = false;
|
|
34
|
+
if (!isOnline) {
|
|
35
|
+
dotColor = '#ef4444'; // red-500
|
|
36
|
+
label = 'Offline';
|
|
37
|
+
}
|
|
38
|
+
else if (isSyncing) {
|
|
39
|
+
dotColor = '#eab308'; // yellow-500
|
|
40
|
+
pulse = true;
|
|
41
|
+
}
|
|
42
|
+
else if (showSyncedLabel) {
|
|
43
|
+
dotColor = '#22c55e'; // green-500
|
|
44
|
+
label = 'Synced';
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
dotColor = '#22c55e'; // green-500
|
|
48
|
+
}
|
|
49
|
+
const dotStyle = {
|
|
50
|
+
display: 'inline-block',
|
|
51
|
+
width: '8px',
|
|
52
|
+
height: '8px',
|
|
53
|
+
borderRadius: '50%',
|
|
54
|
+
backgroundColor: dotColor,
|
|
55
|
+
animation: pulse ? 'revealui-pulse 1.5s ease-in-out infinite' : undefined,
|
|
56
|
+
};
|
|
57
|
+
const containerStyle = {
|
|
58
|
+
display: 'inline-flex',
|
|
59
|
+
alignItems: 'center',
|
|
60
|
+
gap: '6px',
|
|
61
|
+
fontSize: '12px',
|
|
62
|
+
lineHeight: '1',
|
|
63
|
+
};
|
|
64
|
+
return (_jsxs("span", { className: className, style: containerStyle, role: "status", "aria-label": label ?? 'Online', children: [_jsx("span", { style: dotStyle }), label !== null ? _jsx("span", { children: label }) : null, pulse ? (_jsx("style", { children: '@keyframes revealui-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }' })) : null] }));
|
|
65
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fetch wrapper that aborts requests after {@link SHAPE_FETCH_TIMEOUT_MS} (10 s).
|
|
3
|
+
* Passed as `fetchClient` to ElectricSQL `useShape` so that shape subscription
|
|
4
|
+
* requests to the CMS proxy do not hang indefinitely.
|
|
5
|
+
*/
|
|
6
|
+
export declare function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
7
|
+
//# sourceMappingURL=fetch-with-timeout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-with-timeout.d.ts","sourceRoot":"","sources":["../src/fetch-with-timeout.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAoBhG"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Default timeout for Electric shape proxy fetch requests (milliseconds). */
|
|
2
|
+
const SHAPE_FETCH_TIMEOUT_MS = 10_000;
|
|
3
|
+
/**
|
|
4
|
+
* A fetch wrapper that aborts requests after {@link SHAPE_FETCH_TIMEOUT_MS} (10 s).
|
|
5
|
+
* Passed as `fetchClient` to ElectricSQL `useShape` so that shape subscription
|
|
6
|
+
* requests to the CMS proxy do not hang indefinitely.
|
|
7
|
+
*/
|
|
8
|
+
export function fetchWithTimeout(input, init) {
|
|
9
|
+
// If the caller already provides a signal, respect it and compose with our timeout.
|
|
10
|
+
const externalSignal = init?.signal;
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
const timeout = setTimeout(() => controller.abort(), SHAPE_FETCH_TIMEOUT_MS);
|
|
13
|
+
// If the external signal aborts, forward to our controller.
|
|
14
|
+
if (externalSignal) {
|
|
15
|
+
if (externalSignal.aborted) {
|
|
16
|
+
controller.abort(externalSignal.reason);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
externalSignal.addEventListener('abort', () => controller.abort(externalSignal.reason), {
|
|
20
|
+
once: true,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return fetch(input, { ...init, signal: controller.signal }).finally(() => {
|
|
25
|
+
clearTimeout(timeout);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,YAAY,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MutationResult } from '../mutations.js';
|
|
2
|
+
export interface AgentContextRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
session_id: string;
|
|
5
|
+
agent_id: string;
|
|
6
|
+
context: Record<string, unknown>;
|
|
7
|
+
priority: number;
|
|
8
|
+
created_at: string;
|
|
9
|
+
updated_at: string;
|
|
10
|
+
}
|
|
11
|
+
export interface CreateAgentContextInput {
|
|
12
|
+
agent_id: string;
|
|
13
|
+
context?: Record<string, unknown>;
|
|
14
|
+
priority?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface UpdateAgentContextInput {
|
|
17
|
+
context?: Record<string, unknown>;
|
|
18
|
+
priority?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface UseAgentContextsResult {
|
|
21
|
+
contexts: AgentContextRecord[];
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
error: Error | null;
|
|
24
|
+
create: (data: CreateAgentContextInput) => Promise<MutationResult<AgentContextRecord>>;
|
|
25
|
+
update: (id: string, data: UpdateAgentContextInput) => Promise<MutationResult<AgentContextRecord>>;
|
|
26
|
+
remove: (id: string) => Promise<MutationResult<void>>;
|
|
27
|
+
}
|
|
28
|
+
export declare function useAgentContexts(): UseAgentContextsResult;
|
|
29
|
+
//# sourceMappingURL=useAgentContexts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAgentContexts.d.ts","sourceRoot":"","sources":["../../src/hooks/useAgentContexts.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAKtD,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,OAAO,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC,CAAC;IACvF,MAAM,EAAE,CACN,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,uBAAuB,KAC1B,OAAO,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC,CAAC;IACjD,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC;CACvD;AAED,wBAAgB,gBAAgB,IAAI,sBAAsB,CAqBzD"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useShape } from '@electric-sql/react';
|
|
3
|
+
import { fetchWithTimeout } from '../fetch-with-timeout.js';
|
|
4
|
+
import { useSyncMutations } from '../mutations.js';
|
|
5
|
+
import { useElectricConfig } from '../provider/index.js';
|
|
6
|
+
import { toRecords } from '../shape-utils.js';
|
|
7
|
+
export function useAgentContexts() {
|
|
8
|
+
const { proxyBaseUrl } = useElectricConfig();
|
|
9
|
+
const { data, isLoading, error } = useShape({
|
|
10
|
+
url: `${proxyBaseUrl}/api/shapes/agent-contexts`,
|
|
11
|
+
fetchClient: fetchWithTimeout,
|
|
12
|
+
});
|
|
13
|
+
const { create, update, remove } = useSyncMutations('agent-contexts');
|
|
14
|
+
return {
|
|
15
|
+
contexts: toRecords(data),
|
|
16
|
+
isLoading,
|
|
17
|
+
error: error || null,
|
|
18
|
+
create,
|
|
19
|
+
update,
|
|
20
|
+
remove,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { MutationResult } from '../mutations.js';
|
|
2
|
+
export interface AgentMemoryRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
agent_id: string;
|
|
5
|
+
content: string;
|
|
6
|
+
type: string;
|
|
7
|
+
metadata: Record<string, unknown>;
|
|
8
|
+
created_at: string;
|
|
9
|
+
expires_at: string | null;
|
|
10
|
+
}
|
|
11
|
+
export interface CreateAgentMemoryInput {
|
|
12
|
+
agent_id: string;
|
|
13
|
+
content: string;
|
|
14
|
+
type: string;
|
|
15
|
+
source: Record<string, unknown>;
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
expires_at?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface UpdateAgentMemoryInput {
|
|
20
|
+
content?: string;
|
|
21
|
+
type?: string;
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
23
|
+
expires_at?: string | null;
|
|
24
|
+
}
|
|
25
|
+
export interface UseAgentMemoryResult {
|
|
26
|
+
memories: AgentMemoryRecord[];
|
|
27
|
+
isLoading: boolean;
|
|
28
|
+
error: Error | null;
|
|
29
|
+
create: (data: CreateAgentMemoryInput) => Promise<MutationResult<AgentMemoryRecord>>;
|
|
30
|
+
update: (id: string, data: UpdateAgentMemoryInput) => Promise<MutationResult<AgentMemoryRecord>>;
|
|
31
|
+
remove: (id: string) => Promise<MutationResult<void>>;
|
|
32
|
+
}
|
|
33
|
+
export declare function useAgentMemory(agentId: string): UseAgentMemoryResult;
|
|
34
|
+
//# sourceMappingURL=useAgentMemory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAgentMemory.d.ts","sourceRoot":"","sources":["../../src/hooks/useAgentMemory.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOtD,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,sBAAsB,KAAK,OAAO,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC,CAAC;IACrF,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,sBAAsB,KAAK,OAAO,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC,CAAC;IACjG,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC;CACvD;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,oBAAoB,CAuCpE"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useShape } from '@electric-sql/react';
|
|
3
|
+
import { fetchWithTimeout } from '../fetch-with-timeout.js';
|
|
4
|
+
import { useSyncMutations } from '../mutations.js';
|
|
5
|
+
import { useElectricConfig } from '../provider/index.js';
|
|
6
|
+
import { toRecords } from '../shape-utils.js';
|
|
7
|
+
const AGENT_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
|
8
|
+
export function useAgentMemory(agentId) {
|
|
9
|
+
const { proxyBaseUrl } = useElectricConfig();
|
|
10
|
+
const isValid = agentId.length > 0 && AGENT_ID_RE.test(agentId);
|
|
11
|
+
// Hook must always be called (Rules of Hooks). Pass an impossible WHERE when
|
|
12
|
+
// the ID is invalid so the shape returns no rows but the hook still runs.
|
|
13
|
+
const { data, isLoading, error } = useShape({
|
|
14
|
+
url: `${proxyBaseUrl}/api/shapes/agent-memories`,
|
|
15
|
+
params: { agent_id: isValid ? agentId : '00000000-0000-0000-0000-000000000000' },
|
|
16
|
+
fetchClient: fetchWithTimeout,
|
|
17
|
+
});
|
|
18
|
+
const { create, update, remove } = useSyncMutations('agent-memories');
|
|
19
|
+
if (!isValid) {
|
|
20
|
+
return {
|
|
21
|
+
memories: [],
|
|
22
|
+
isLoading: false,
|
|
23
|
+
error: new Error('Invalid agentId: must be non-empty alphanumeric, hyphens, underscores only'),
|
|
24
|
+
create,
|
|
25
|
+
update,
|
|
26
|
+
remove,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
memories: toRecords(data),
|
|
31
|
+
isLoading,
|
|
32
|
+
error: error || null,
|
|
33
|
+
create,
|
|
34
|
+
update,
|
|
35
|
+
remove,
|
|
36
|
+
};
|
|
37
|
+
}
|