@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.
Files changed (75) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +201 -0
  3. package/dist/collab/agent-client-factory.d.ts +16 -0
  4. package/dist/collab/agent-client-factory.d.ts.map +1 -0
  5. package/dist/collab/agent-client-factory.js +24 -0
  6. package/dist/collab/agent-client.d.ts +56 -0
  7. package/dist/collab/agent-client.d.ts.map +1 -0
  8. package/dist/collab/agent-client.js +262 -0
  9. package/dist/collab/index.d.ts +8 -0
  10. package/dist/collab/index.d.ts.map +1 -0
  11. package/dist/collab/index.js +4 -0
  12. package/dist/collab/protocol-constants.d.ts +5 -0
  13. package/dist/collab/protocol-constants.d.ts.map +1 -0
  14. package/dist/collab/protocol-constants.js +4 -0
  15. package/dist/collab/server-index.d.ts +6 -0
  16. package/dist/collab/server-index.d.ts.map +1 -0
  17. package/dist/collab/server-index.js +3 -0
  18. package/dist/collab/use-collab-document.d.ts +8 -0
  19. package/dist/collab/use-collab-document.d.ts.map +1 -0
  20. package/dist/collab/use-collab-document.js +49 -0
  21. package/dist/collab/use-collaboration.d.ts +26 -0
  22. package/dist/collab/use-collaboration.d.ts.map +1 -0
  23. package/dist/collab/use-collaboration.js +65 -0
  24. package/dist/collab/yjs-websocket-provider.d.ts +36 -0
  25. package/dist/collab/yjs-websocket-provider.d.ts.map +1 -0
  26. package/dist/collab/yjs-websocket-provider.js +172 -0
  27. package/dist/components/SyncStatusIndicator.d.ts +17 -0
  28. package/dist/components/SyncStatusIndicator.d.ts.map +1 -0
  29. package/dist/components/SyncStatusIndicator.js +65 -0
  30. package/dist/fetch-with-timeout.d.ts +7 -0
  31. package/dist/fetch-with-timeout.d.ts.map +1 -0
  32. package/dist/fetch-with-timeout.js +27 -0
  33. package/dist/hooks/index.d.ts +4 -0
  34. package/dist/hooks/index.d.ts.map +1 -0
  35. package/dist/hooks/index.js +2 -0
  36. package/dist/hooks/useAgentContexts.d.ts +29 -0
  37. package/dist/hooks/useAgentContexts.d.ts.map +1 -0
  38. package/dist/hooks/useAgentContexts.js +22 -0
  39. package/dist/hooks/useAgentMemory.d.ts +34 -0
  40. package/dist/hooks/useAgentMemory.d.ts.map +1 -0
  41. package/dist/hooks/useAgentMemory.js +37 -0
  42. package/dist/hooks/useConversations.d.ts +31 -0
  43. package/dist/hooks/useConversations.d.ts.map +1 -0
  44. package/dist/hooks/useConversations.js +26 -0
  45. package/dist/hooks/useCoordinationSessions.d.ts +35 -0
  46. package/dist/hooks/useCoordinationSessions.d.ts.map +1 -0
  47. package/dist/hooks/useCoordinationSessions.js +22 -0
  48. package/dist/hooks/useCoordinationWorkItems.d.ts +41 -0
  49. package/dist/hooks/useCoordinationWorkItems.d.ts.map +1 -0
  50. package/dist/hooks/useCoordinationWorkItems.js +22 -0
  51. package/dist/hooks/useOfflineCache.d.ts +32 -0
  52. package/dist/hooks/useOfflineCache.d.ts.map +1 -0
  53. package/dist/hooks/useOfflineCache.js +129 -0
  54. package/dist/hooks/useOnlineStatus.d.ts +21 -0
  55. package/dist/hooks/useOnlineStatus.d.ts.map +1 -0
  56. package/dist/hooks/useOnlineStatus.js +74 -0
  57. package/dist/index.d.ts +26 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +18 -0
  60. package/dist/mutations.d.ts +14 -0
  61. package/dist/mutations.d.ts.map +1 -0
  62. package/dist/mutations.js +53 -0
  63. package/dist/offline-queue.d.ts +55 -0
  64. package/dist/offline-queue.d.ts.map +1 -0
  65. package/dist/offline-queue.js +126 -0
  66. package/dist/provider/index.d.ts +35 -0
  67. package/dist/provider/index.d.ts.map +1 -0
  68. package/dist/provider/index.js +29 -0
  69. package/dist/shape-utils.d.ts +11 -0
  70. package/dist/shape-utils.d.ts.map +1 -0
  71. package/dist/shape-utils.js +14 -0
  72. package/dist/test-setup.d.ts +2 -0
  73. package/dist/test-setup.d.ts.map +1 -0
  74. package/dist/test-setup.js +1 -0
  75. 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,4 @@
1
+ export { MESSAGE_AWARENESS, MESSAGE_SYNC } from './protocol-constants.js';
2
+ export { useCollabDocument } from './use-collab-document.js';
3
+ export { useCollaboration } from './use-collaboration.js';
4
+ export { CollabProvider } from './yjs-websocket-provider.js';
@@ -0,0 +1,5 @@
1
+ /** Yjs sync protocol message type */
2
+ export declare const MESSAGE_SYNC = 0;
3
+ /** Yjs awareness protocol message type */
4
+ export declare const MESSAGE_AWARENESS = 1;
5
+ //# sourceMappingURL=protocol-constants.d.ts.map
@@ -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,4 @@
1
+ /** Yjs sync protocol message type */
2
+ export const MESSAGE_SYNC = 0;
3
+ /** Yjs awareness protocol message type */
4
+ export const MESSAGE_AWARENESS = 1;
@@ -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,3 @@
1
+ export { AgentCollabClient } from './agent-client.js';
2
+ export { createAgentClient, createAndConnectAgentClient } from './agent-client-factory.js';
3
+ export { MESSAGE_AWARENESS, MESSAGE_SYNC } from './protocol-constants.js';
@@ -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"}