@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
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 RevealUI Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# @revealui/sync
|
|
2
|
+
|
|
3
|
+
ElectricSQL sync utilities for RevealUI — real-time data synchronization with local-first architecture.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **ElectricSQL Integration**: Real-time sync via shape subscriptions
|
|
8
|
+
- **React Hooks**: Subscribe to synced data in React components
|
|
9
|
+
- **Mutations**: Create, update, and delete records via authenticated REST endpoints
|
|
10
|
+
- **Type-safe**: Full TypeScript support with database types
|
|
11
|
+
- **React Provider**: Easy setup with `ElectricProvider`
|
|
12
|
+
- **Yjs Collaboration**: CRDT-based real-time collaborative editing
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add @revealui/sync
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Setup Provider
|
|
23
|
+
|
|
24
|
+
Wrap your app with `ElectricProvider`:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { ElectricProvider } from '@revealui/sync/provider'
|
|
28
|
+
|
|
29
|
+
export default function App() {
|
|
30
|
+
return (
|
|
31
|
+
<ElectricProvider proxyBaseUrl="https://cms.revealui.com">
|
|
32
|
+
<YourComponents />
|
|
33
|
+
</ElectricProvider>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Read Synced Data
|
|
39
|
+
|
|
40
|
+
Hooks subscribe to ElectricSQL shapes via authenticated proxy routes. Data updates in real-time as the database changes.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { useAgentContexts } from '@revealui/sync'
|
|
44
|
+
|
|
45
|
+
function MyComponent() {
|
|
46
|
+
const { contexts, isLoading } = useAgentContexts()
|
|
47
|
+
|
|
48
|
+
if (isLoading) return <div>Loading...</div>
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<ul>
|
|
52
|
+
{contexts.map(context => (
|
|
53
|
+
<li key={context.id}>{JSON.stringify(context.context)}</li>
|
|
54
|
+
))}
|
|
55
|
+
</ul>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Mutations
|
|
61
|
+
|
|
62
|
+
Each hook returns `create`, `update`, and `remove` functions. Mutations go through authenticated REST endpoints at `/api/sync/*`. ElectricSQL picks up the database changes and pushes updates to all subscribers automatically.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { useAgentContexts } from '@revealui/sync'
|
|
66
|
+
|
|
67
|
+
function CreateContext() {
|
|
68
|
+
const { contexts, create, update, remove } = useAgentContexts()
|
|
69
|
+
|
|
70
|
+
const handleCreate = async () => {
|
|
71
|
+
const result = await create({
|
|
72
|
+
agent_id: 'assistant',
|
|
73
|
+
context: { theme: 'dark', language: 'en' },
|
|
74
|
+
priority: 0.8,
|
|
75
|
+
})
|
|
76
|
+
if (!result.success) console.error(result.error)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const handleUpdate = async (id: string) => {
|
|
80
|
+
await update(id, { context: { theme: 'light' } })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const handleDelete = async (id: string) => {
|
|
84
|
+
await remove(id)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return <button onClick={handleCreate}>Create</button>
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Available Hooks
|
|
92
|
+
|
|
93
|
+
### `useAgentContexts()`
|
|
94
|
+
|
|
95
|
+
Subscribe to agent contexts (task context, working memory).
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
const {
|
|
99
|
+
contexts, // AgentContextRecord[]
|
|
100
|
+
isLoading, // boolean
|
|
101
|
+
error, // Error | null
|
|
102
|
+
create, // (data: CreateAgentContextInput) => Promise<MutationResult>
|
|
103
|
+
update, // (id: string, data: UpdateAgentContextInput) => Promise<MutationResult>
|
|
104
|
+
remove, // (id: string) => Promise<MutationResult>
|
|
105
|
+
} = useAgentContexts()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### `useAgentMemory(agentId)`
|
|
109
|
+
|
|
110
|
+
Subscribe to agent memory (episodic, semantic, working) filtered by agent ID.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const {
|
|
114
|
+
memories, // AgentMemoryRecord[]
|
|
115
|
+
isLoading, // boolean
|
|
116
|
+
error, // Error | null
|
|
117
|
+
create, // (data: CreateAgentMemoryInput) => Promise<MutationResult>
|
|
118
|
+
update, // (id: string, data: UpdateAgentMemoryInput) => Promise<MutationResult>
|
|
119
|
+
remove, // (id: string) => Promise<MutationResult>
|
|
120
|
+
} = useAgentMemory('assistant')
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `useConversations(userId)`
|
|
124
|
+
|
|
125
|
+
Subscribe to conversation history. Server-side proxy enforces row-level filtering by session — the `userId` parameter is for API compatibility but filtering is handled server-side.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const {
|
|
129
|
+
conversations, // ConversationRecord[]
|
|
130
|
+
isLoading, // boolean
|
|
131
|
+
error, // Error | null
|
|
132
|
+
create, // (data: CreateConversationInput) => Promise<MutationResult>
|
|
133
|
+
update, // (id: string, data: UpdateConversationInput) => Promise<MutationResult>
|
|
134
|
+
remove, // (id: string) => Promise<MutationResult>
|
|
135
|
+
} = useConversations(userId)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## How It Works
|
|
139
|
+
|
|
140
|
+
1. **Reads**: ElectricSQL shape subscriptions via authenticated CMS proxy (`/api/shapes/*`)
|
|
141
|
+
2. **Writes**: REST mutations via CMS API (`/api/sync/*`) → Postgres → ElectricSQL replication
|
|
142
|
+
3. **Real-time**: Database changes propagate to all shape subscribers automatically
|
|
143
|
+
4. **Auth**: All endpoints require a valid session cookie
|
|
144
|
+
|
|
145
|
+
## Collaboration (Yjs)
|
|
146
|
+
|
|
147
|
+
The collab layer provides CRDT-based collaborative editing:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { useCollaboration } from '@revealui/sync'
|
|
151
|
+
|
|
152
|
+
function Editor() {
|
|
153
|
+
const { doc, synced, connectedUsers } = useCollaboration({
|
|
154
|
+
documentId: 'doc-uuid',
|
|
155
|
+
serverUrl: 'ws://localhost:4000',
|
|
156
|
+
})
|
|
157
|
+
// ...
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Server-side agents can use `AgentCollabClient` from `@revealui/sync/collab/server`.
|
|
162
|
+
|
|
163
|
+
## Environment Variables
|
|
164
|
+
|
|
165
|
+
```env
|
|
166
|
+
# ElectricSQL service URL (used by CMS proxy)
|
|
167
|
+
ELECTRIC_SERVICE_URL=http://localhost:5133
|
|
168
|
+
|
|
169
|
+
# Optional: Electric auth secret
|
|
170
|
+
ELECTRIC_SECRET=your-secret
|
|
171
|
+
|
|
172
|
+
# Client-side (stored in provider context)
|
|
173
|
+
NEXT_PUBLIC_ELECTRIC_SERVICE_URL=http://localhost:5133
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pnpm --filter @revealui/sync build # Build
|
|
180
|
+
pnpm --filter @revealui/sync dev # Watch mode
|
|
181
|
+
pnpm --filter @revealui/sync test # Run tests
|
|
182
|
+
pnpm --filter @revealui/sync typecheck # Type check
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## When to Use This
|
|
186
|
+
|
|
187
|
+
- You need real-time data sync between your database and React UI via ElectricSQL
|
|
188
|
+
- You want CRDT-based collaborative editing (Yjs) for multi-user document workflows
|
|
189
|
+
- You need React hooks that subscribe to live database changes with automatic mutation support
|
|
190
|
+
- **Not** for batch data loading or static pages — use server components with `@revealui/db` directly
|
|
191
|
+
- **Not** for offline-first mobile apps — ElectricSQL targets web clients with persistent connections
|
|
192
|
+
|
|
193
|
+
## JOSHUA Alignment
|
|
194
|
+
|
|
195
|
+
- **Adaptive**: Shape subscriptions dynamically sync only the data your component needs — scales from one user to many
|
|
196
|
+
- **Sovereign**: Sync runs through your own CMS proxy and PostgreSQL — no third-party real-time service required
|
|
197
|
+
- **Hermetic**: All mutations go through authenticated REST endpoints; ElectricSQL replication is read-only on the client
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AgentCollabClient } from './agent-client.js';
|
|
2
|
+
export interface CreateAgentClientOptions {
|
|
3
|
+
serverUrl: string;
|
|
4
|
+
documentId: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
color?: string;
|
|
8
|
+
authToken?: string;
|
|
9
|
+
autoReconnect?: boolean;
|
|
10
|
+
defaultTextName?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function createAgentClient(options: CreateAgentClientOptions): AgentCollabClient;
|
|
13
|
+
export declare function createAndConnectAgentClient(options: CreateAgentClientOptions & {
|
|
14
|
+
syncTimeoutMs?: number;
|
|
15
|
+
}): Promise<AgentCollabClient>;
|
|
16
|
+
//# sourceMappingURL=agent-client-factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-client-factory.d.ts","sourceRoot":"","sources":["../../src/collab/agent-client-factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAsB,MAAM,mBAAmB,CAAC;AAI1E,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CAgBtF;AAED,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,wBAAwB,GAAG;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7D,OAAO,CAAC,iBAAiB,CAAC,CAK5B"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AgentCollabClient } from './agent-client.js';
|
|
2
|
+
const DEFAULT_AGENT_COLOR = '#8B5CF6';
|
|
3
|
+
export function createAgentClient(options) {
|
|
4
|
+
const identity = {
|
|
5
|
+
type: 'agent',
|
|
6
|
+
name: options.name ?? 'AI Agent',
|
|
7
|
+
model: options.model ?? 'unknown',
|
|
8
|
+
color: options.color ?? DEFAULT_AGENT_COLOR,
|
|
9
|
+
};
|
|
10
|
+
return new AgentCollabClient({
|
|
11
|
+
serverUrl: options.serverUrl,
|
|
12
|
+
documentId: options.documentId,
|
|
13
|
+
identity,
|
|
14
|
+
authToken: options.authToken,
|
|
15
|
+
autoReconnect: options.autoReconnect,
|
|
16
|
+
defaultTextName: options.defaultTextName,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export async function createAndConnectAgentClient(options) {
|
|
20
|
+
const client = createAgentClient(options);
|
|
21
|
+
client.connect();
|
|
22
|
+
await client.waitForSync(options.syncTimeoutMs ?? 5000);
|
|
23
|
+
return client;
|
|
24
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Observable } from 'lib0/observable';
|
|
2
|
+
import * as awarenessProtocol from 'y-protocols/awareness';
|
|
3
|
+
import * as Y from 'yjs';
|
|
4
|
+
export interface AgentIdentity {
|
|
5
|
+
type: 'agent';
|
|
6
|
+
name: string;
|
|
7
|
+
model: string;
|
|
8
|
+
color: string;
|
|
9
|
+
}
|
|
10
|
+
export interface AgentCollabClientOptions {
|
|
11
|
+
serverUrl: string;
|
|
12
|
+
documentId: string;
|
|
13
|
+
identity: AgentIdentity;
|
|
14
|
+
authToken?: string;
|
|
15
|
+
autoReconnect?: boolean;
|
|
16
|
+
defaultTextName?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class AgentCollabClient extends Observable<string> {
|
|
19
|
+
readonly doc: Y.Doc;
|
|
20
|
+
readonly awareness: awarenessProtocol.Awareness;
|
|
21
|
+
private readonly serverUrl;
|
|
22
|
+
private readonly documentId;
|
|
23
|
+
private readonly identity;
|
|
24
|
+
private readonly authToken?;
|
|
25
|
+
private readonly autoReconnect;
|
|
26
|
+
private readonly defaultTextName;
|
|
27
|
+
private ws;
|
|
28
|
+
private synced;
|
|
29
|
+
private reconnectAttempts;
|
|
30
|
+
private reconnectTimer;
|
|
31
|
+
private updateHandler;
|
|
32
|
+
private awarenessUpdateHandler;
|
|
33
|
+
private destroyed;
|
|
34
|
+
private pendingSyncAbort;
|
|
35
|
+
constructor(options: AgentCollabClientOptions);
|
|
36
|
+
connect(): void;
|
|
37
|
+
disconnect(): void;
|
|
38
|
+
getDocument(): Y.Doc;
|
|
39
|
+
getText(name?: string): Y.Text;
|
|
40
|
+
getTextContent(name?: string): string;
|
|
41
|
+
insertText(index: number, content: string, name?: string): void;
|
|
42
|
+
deleteText(index: number, length: number, name?: string): void;
|
|
43
|
+
replaceAll(content: string, name?: string): void;
|
|
44
|
+
onUpdate(callback: (update: Uint8Array) => void): () => void;
|
|
45
|
+
getConnectedUsers(): Map<number, Record<string, unknown>>;
|
|
46
|
+
waitForSync(timeoutMs?: number): Promise<void>;
|
|
47
|
+
destroy(): void;
|
|
48
|
+
private buildWebSocketUrl;
|
|
49
|
+
private handleOpen;
|
|
50
|
+
private handleMessage;
|
|
51
|
+
private handleClose;
|
|
52
|
+
private handleError;
|
|
53
|
+
private scheduleReconnect;
|
|
54
|
+
private cancelReconnect;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=agent-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-client.d.ts","sourceRoot":"","sources":["../../src/collab/agent-client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,KAAK,iBAAiB,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAOzB,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,aAAa,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,iBAAkB,SAAQ,UAAU,CAAC,MAAM,CAAC;IACvD,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;IACpB,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC,SAAS,CAAC;IAChD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAU;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IAEzC,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;IACV,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,gBAAgB,CAA6B;gBAEzC,OAAO,EAAE,wBAAwB;IA0B7C,OAAO,IAAI,IAAI;IAcf,UAAU,IAAI,IAAI;IAkBlB,WAAW,IAAI,CAAC,CAAC,GAAG;IAIpB,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI;IAI9B,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM;IAIrC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAI/D,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAI9D,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAQhD,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI;IAU5D,iBAAiB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAUzD,WAAW,CAAC,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA+B5C,OAAO,IAAI,IAAI;IAUf,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,UAAU;IAoClB,OAAO,CAAC,aAAa;IAoCrB,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,eAAe;CAOxB"}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import * as decoding from 'lib0/decoding';
|
|
2
|
+
import * as encoding from 'lib0/encoding';
|
|
3
|
+
import { Observable } from 'lib0/observable';
|
|
4
|
+
import WebSocket from 'ws';
|
|
5
|
+
import * as awarenessProtocol from 'y-protocols/awareness';
|
|
6
|
+
import * as syncProtocol from 'y-protocols/sync';
|
|
7
|
+
import * as Y from 'yjs';
|
|
8
|
+
import { MESSAGE_AWARENESS, MESSAGE_SYNC } from './protocol-constants.js';
|
|
9
|
+
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
10
|
+
const BASE_RECONNECT_DELAY = 1000;
|
|
11
|
+
const RECONNECT_MULTIPLIER = 2;
|
|
12
|
+
export class AgentCollabClient extends Observable {
|
|
13
|
+
doc;
|
|
14
|
+
awareness;
|
|
15
|
+
serverUrl;
|
|
16
|
+
documentId;
|
|
17
|
+
identity;
|
|
18
|
+
authToken;
|
|
19
|
+
autoReconnect;
|
|
20
|
+
defaultTextName;
|
|
21
|
+
ws = null;
|
|
22
|
+
synced = false;
|
|
23
|
+
reconnectAttempts = 0;
|
|
24
|
+
reconnectTimer = null;
|
|
25
|
+
updateHandler = null;
|
|
26
|
+
awarenessUpdateHandler;
|
|
27
|
+
destroyed = false;
|
|
28
|
+
pendingSyncAbort = null;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
super();
|
|
31
|
+
this.serverUrl = options.serverUrl;
|
|
32
|
+
this.documentId = options.documentId;
|
|
33
|
+
this.identity = options.identity;
|
|
34
|
+
this.authToken = options.authToken;
|
|
35
|
+
this.autoReconnect = options.autoReconnect ?? true;
|
|
36
|
+
this.defaultTextName = options.defaultTextName ?? 'content';
|
|
37
|
+
this.doc = new Y.Doc();
|
|
38
|
+
this.awareness = new awarenessProtocol.Awareness(this.doc);
|
|
39
|
+
this.awarenessUpdateHandler = ({ added, updated, removed }, origin) => {
|
|
40
|
+
if (origin === 'remote')
|
|
41
|
+
return;
|
|
42
|
+
const changedClients = added.concat(updated, removed);
|
|
43
|
+
const update = awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients);
|
|
44
|
+
const encoder = encoding.createEncoder();
|
|
45
|
+
encoding.writeVarUint(encoder, MESSAGE_AWARENESS);
|
|
46
|
+
encoding.writeVarUint8Array(encoder, update);
|
|
47
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
48
|
+
this.ws.send(encoding.toUint8Array(encoder));
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
this.awareness.on('update', this.awarenessUpdateHandler);
|
|
52
|
+
}
|
|
53
|
+
connect() {
|
|
54
|
+
if (this.ws || this.destroyed)
|
|
55
|
+
return;
|
|
56
|
+
const url = this.buildWebSocketUrl();
|
|
57
|
+
this.ws = new WebSocket(url);
|
|
58
|
+
this.emit('status', [{ status: 'connecting' }]);
|
|
59
|
+
this.ws.on('open', () => this.handleOpen());
|
|
60
|
+
this.ws.on('message', (data) => this.handleMessage(data));
|
|
61
|
+
this.ws.on('close', () => this.handleClose());
|
|
62
|
+
this.ws.on('error', (err) => this.handleError(err));
|
|
63
|
+
}
|
|
64
|
+
disconnect() {
|
|
65
|
+
this.cancelReconnect();
|
|
66
|
+
if (this.updateHandler) {
|
|
67
|
+
this.doc.off('update', this.updateHandler);
|
|
68
|
+
this.updateHandler = null;
|
|
69
|
+
}
|
|
70
|
+
awarenessProtocol.removeAwarenessStates(this.awareness, [this.doc.clientID], 'local');
|
|
71
|
+
if (this.ws) {
|
|
72
|
+
this.ws.removeAllListeners();
|
|
73
|
+
this.ws.close();
|
|
74
|
+
this.ws = null;
|
|
75
|
+
}
|
|
76
|
+
this.synced = false;
|
|
77
|
+
this.emit('status', [{ status: 'disconnected' }]);
|
|
78
|
+
}
|
|
79
|
+
getDocument() {
|
|
80
|
+
return this.doc;
|
|
81
|
+
}
|
|
82
|
+
getText(name) {
|
|
83
|
+
return this.doc.getText(name ?? this.defaultTextName);
|
|
84
|
+
}
|
|
85
|
+
getTextContent(name) {
|
|
86
|
+
return this.getText(name).toString();
|
|
87
|
+
}
|
|
88
|
+
insertText(index, content, name) {
|
|
89
|
+
this.getText(name).insert(index, content);
|
|
90
|
+
}
|
|
91
|
+
deleteText(index, length, name) {
|
|
92
|
+
this.getText(name).delete(index, length);
|
|
93
|
+
}
|
|
94
|
+
replaceAll(content, name) {
|
|
95
|
+
const text = this.getText(name);
|
|
96
|
+
this.doc.transact(() => {
|
|
97
|
+
text.delete(0, text.length);
|
|
98
|
+
text.insert(0, content);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
onUpdate(callback) {
|
|
102
|
+
const handler = (update, origin) => {
|
|
103
|
+
if (origin !== this) {
|
|
104
|
+
callback(update);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
this.doc.on('update', handler);
|
|
108
|
+
return () => this.doc.off('update', handler);
|
|
109
|
+
}
|
|
110
|
+
getConnectedUsers() {
|
|
111
|
+
const users = new Map();
|
|
112
|
+
for (const [clientId, state] of this.awareness.getStates()) {
|
|
113
|
+
if (state && typeof state === 'object') {
|
|
114
|
+
users.set(clientId, state);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return users;
|
|
118
|
+
}
|
|
119
|
+
waitForSync(timeoutMs = 5000) {
|
|
120
|
+
if (this.synced)
|
|
121
|
+
return Promise.resolve();
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const cleanup = () => {
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
this.off('sync', handler);
|
|
126
|
+
if (this.pendingSyncAbort === abort)
|
|
127
|
+
this.pendingSyncAbort = null;
|
|
128
|
+
};
|
|
129
|
+
const timeout = setTimeout(() => {
|
|
130
|
+
cleanup();
|
|
131
|
+
reject(new Error(`Sync timeout after ${timeoutMs}ms`));
|
|
132
|
+
}, timeoutMs);
|
|
133
|
+
const handler = (isSynced) => {
|
|
134
|
+
if (isSynced) {
|
|
135
|
+
cleanup();
|
|
136
|
+
resolve();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const abort = () => {
|
|
140
|
+
cleanup();
|
|
141
|
+
reject(new Error('Client destroyed while waiting for sync'));
|
|
142
|
+
};
|
|
143
|
+
this.pendingSyncAbort = abort;
|
|
144
|
+
this.on('sync', handler);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
destroy() {
|
|
148
|
+
this.destroyed = true;
|
|
149
|
+
this.pendingSyncAbort?.();
|
|
150
|
+
this.awareness.off('update', this.awarenessUpdateHandler);
|
|
151
|
+
this.disconnect();
|
|
152
|
+
this.awareness.destroy();
|
|
153
|
+
this.doc.destroy();
|
|
154
|
+
super.destroy();
|
|
155
|
+
}
|
|
156
|
+
buildWebSocketUrl() {
|
|
157
|
+
const params = new URLSearchParams({
|
|
158
|
+
name: this.identity.name,
|
|
159
|
+
color: this.identity.color,
|
|
160
|
+
type: 'agent',
|
|
161
|
+
agentModel: this.identity.model,
|
|
162
|
+
});
|
|
163
|
+
if (this.authToken) {
|
|
164
|
+
params.set('token', this.authToken);
|
|
165
|
+
}
|
|
166
|
+
return `${this.serverUrl}/ws/collab/${this.documentId}?${params.toString()}`;
|
|
167
|
+
}
|
|
168
|
+
handleOpen() {
|
|
169
|
+
this.reconnectAttempts = 0;
|
|
170
|
+
this.emit('status', [{ status: 'connected' }]);
|
|
171
|
+
const encoder = encoding.createEncoder();
|
|
172
|
+
encoding.writeVarUint(encoder, MESSAGE_SYNC);
|
|
173
|
+
syncProtocol.writeSyncStep1(encoder, this.doc);
|
|
174
|
+
this.ws?.send(encoding.toUint8Array(encoder));
|
|
175
|
+
this.awareness.setLocalState({
|
|
176
|
+
name: this.identity.name,
|
|
177
|
+
color: this.identity.color,
|
|
178
|
+
type: 'agent',
|
|
179
|
+
agentModel: this.identity.model,
|
|
180
|
+
});
|
|
181
|
+
const awarenessUpdate = awarenessProtocol.encodeAwarenessUpdate(this.awareness, [
|
|
182
|
+
this.doc.clientID,
|
|
183
|
+
]);
|
|
184
|
+
const awarenessEncoder = encoding.createEncoder();
|
|
185
|
+
encoding.writeVarUint(awarenessEncoder, MESSAGE_AWARENESS);
|
|
186
|
+
encoding.writeVarUint8Array(awarenessEncoder, awarenessUpdate);
|
|
187
|
+
this.ws?.send(encoding.toUint8Array(awarenessEncoder));
|
|
188
|
+
this.updateHandler = (update, origin) => {
|
|
189
|
+
if (origin === this)
|
|
190
|
+
return;
|
|
191
|
+
const updateEncoder = encoding.createEncoder();
|
|
192
|
+
encoding.writeVarUint(updateEncoder, MESSAGE_SYNC);
|
|
193
|
+
syncProtocol.writeUpdate(updateEncoder, update);
|
|
194
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
195
|
+
this.ws.send(encoding.toUint8Array(updateEncoder));
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
this.doc.on('update', this.updateHandler);
|
|
199
|
+
}
|
|
200
|
+
handleMessage(data) {
|
|
201
|
+
const uint8 = data instanceof ArrayBuffer
|
|
202
|
+
? new Uint8Array(data)
|
|
203
|
+
: Array.isArray(data)
|
|
204
|
+
? new Uint8Array(Buffer.concat(data))
|
|
205
|
+
: new Uint8Array(data);
|
|
206
|
+
const decoder = decoding.createDecoder(uint8);
|
|
207
|
+
const messageType = decoding.readVarUint(decoder);
|
|
208
|
+
if (messageType === MESSAGE_SYNC) {
|
|
209
|
+
const responseEncoder = encoding.createEncoder();
|
|
210
|
+
encoding.writeVarUint(responseEncoder, MESSAGE_SYNC);
|
|
211
|
+
const syncMessageType = syncProtocol.readSyncMessage(decoder, responseEncoder, this.doc, this);
|
|
212
|
+
if (encoding.length(responseEncoder) > 1) {
|
|
213
|
+
this.ws?.send(encoding.toUint8Array(responseEncoder));
|
|
214
|
+
}
|
|
215
|
+
if (syncMessageType === 1 && !this.synced) {
|
|
216
|
+
this.synced = true;
|
|
217
|
+
}
|
|
218
|
+
this.emit('sync', [true]);
|
|
219
|
+
}
|
|
220
|
+
else if (messageType === MESSAGE_AWARENESS) {
|
|
221
|
+
const update = decoding.readVarUint8Array(decoder);
|
|
222
|
+
awarenessProtocol.applyAwarenessUpdate(this.awareness, update, 'remote');
|
|
223
|
+
this.emit('awareness', [this.getConnectedUsers()]);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
handleClose() {
|
|
227
|
+
if (this.updateHandler) {
|
|
228
|
+
this.doc.off('update', this.updateHandler);
|
|
229
|
+
this.updateHandler = null;
|
|
230
|
+
}
|
|
231
|
+
this.ws = null;
|
|
232
|
+
this.synced = false;
|
|
233
|
+
this.emit('sync', [false]);
|
|
234
|
+
this.emit('status', [{ status: 'disconnected' }]);
|
|
235
|
+
if (this.autoReconnect && !this.destroyed) {
|
|
236
|
+
this.scheduleReconnect();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
handleError(err) {
|
|
240
|
+
this.emit('error', [err]);
|
|
241
|
+
this.ws?.close();
|
|
242
|
+
}
|
|
243
|
+
scheduleReconnect() {
|
|
244
|
+
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
245
|
+
this.emit('status', [{ status: 'failed' }]);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const delay = BASE_RECONNECT_DELAY * RECONNECT_MULTIPLIER ** this.reconnectAttempts;
|
|
249
|
+
this.reconnectAttempts++;
|
|
250
|
+
this.reconnectTimer = setTimeout(() => {
|
|
251
|
+
this.reconnectTimer = null;
|
|
252
|
+
this.connect();
|
|
253
|
+
}, delay);
|
|
254
|
+
}
|
|
255
|
+
cancelReconnect() {
|
|
256
|
+
if (this.reconnectTimer) {
|
|
257
|
+
clearTimeout(this.reconnectTimer);
|
|
258
|
+
this.reconnectTimer = null;
|
|
259
|
+
}
|
|
260
|
+
this.reconnectAttempts = 0;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { MESSAGE_AWARENESS, MESSAGE_SYNC } from './protocol-constants.js';
|
|
2
|
+
export type { CollabDocumentState } from './use-collab-document.js';
|
|
3
|
+
export { useCollabDocument } from './use-collab-document.js';
|
|
4
|
+
export type { CollaborationIdentity, UseCollaborationOptions, UseCollaborationResult, } from './use-collaboration.js';
|
|
5
|
+
export { useCollaboration } from './use-collaboration.js';
|
|
6
|
+
export type { UserPresence } from './yjs-websocket-provider.js';
|
|
7
|
+
export { CollabProvider } from './yjs-websocket-provider.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/collab/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC1E,YAAY,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,YAAY,EACV,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol-constants.d.ts","sourceRoot":"","sources":["../../src/collab/protocol-constants.ts"],"names":[],"mappings":"AAAA,qCAAqC;AACrC,eAAO,MAAM,YAAY,IAAI,CAAC;AAE9B,0CAA0C;AAC1C,eAAO,MAAM,iBAAiB,IAAI,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { AgentCollabClientOptions, AgentIdentity } from './agent-client.js';
|
|
2
|
+
export { AgentCollabClient } from './agent-client.js';
|
|
3
|
+
export type { CreateAgentClientOptions } from './agent-client-factory.js';
|
|
4
|
+
export { createAgentClient, createAndConnectAgentClient } from './agent-client-factory.js';
|
|
5
|
+
export { MESSAGE_AWARENESS, MESSAGE_SYNC } from './protocol-constants.js';
|
|
6
|
+
//# sourceMappingURL=server-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-index.d.ts","sourceRoot":"","sources":["../../src/collab/server-index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,wBAAwB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC3F,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface CollabDocumentState {
|
|
2
|
+
initialState: Uint8Array | null;
|
|
3
|
+
connectedClients: number;
|
|
4
|
+
isLoading: boolean;
|
|
5
|
+
error: Error | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function useCollabDocument(documentId: string): CollabDocumentState;
|
|
8
|
+
//# sourceMappingURL=use-collab-document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-collab-document.d.ts","sourceRoot":"","sources":["../../src/collab/use-collab-document.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAKD,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,mBAAmB,CAiDzE"}
|