@revealui/sync 0.2.0
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 +223 -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 +55 -0
- package/dist/collab/agent-client.d.ts.map +1 -0
- package/dist/collab/agent-client.js +253 -0
- package/dist/collab/index.d.ts +7 -0
- package/dist/collab/index.d.ts.map +1 -0
- package/dist/collab/index.js +3 -0
- package/dist/collab/server-index.d.ts +5 -0
- package/dist/collab/server-index.d.ts.map +1 -0
- package/dist/collab/server-index.js +2 -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 +46 -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 +173 -0
- package/dist/hooks/useConversations.d.ts +11 -0
- package/dist/hooks/useConversations.d.ts.map +1 -0
- package/dist/hooks/useConversations.js +16 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/provider/index.d.ts +7 -0
- package/dist/provider/index.d.ts.map +1 -0
- package/dist/provider/index.js +8 -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 +67 -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,223 @@
|
|
|
1
|
+
# @revealui/sync
|
|
2
|
+
|
|
3
|
+
**Status:** 🟡 Active Development | ⚠️ NOT Production Ready
|
|
4
|
+
|
|
5
|
+
See [Project Status](../../docs/PROJECT_STATUS.md) for framework readiness.
|
|
6
|
+
|
|
7
|
+
ElectricSQL sync utilities for RevealUI - real-time data synchronization with local-first architecture.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **ElectricSQL Integration**: Real-time sync with ElectricSQL
|
|
12
|
+
- **React Hooks**: Use sync data in React components
|
|
13
|
+
- **Type-safe**: Full TypeScript support with database types
|
|
14
|
+
- **Local-first**: Works offline, syncs when online
|
|
15
|
+
- **React Provider**: Easy setup with `ElectricProvider`
|
|
16
|
+
- **Optimistic Updates**: Instant UI updates with server reconciliation
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add @revealui/sync
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Setup Provider
|
|
27
|
+
|
|
28
|
+
Wrap your app with `ElectricProvider`:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { ElectricProvider } from '@revealui/sync/provider'
|
|
32
|
+
|
|
33
|
+
export default function App() {
|
|
34
|
+
return (
|
|
35
|
+
<ElectricProvider>
|
|
36
|
+
<YourComponents />
|
|
37
|
+
</ElectricProvider>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Use Synced Data
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { useAgentContexts } from '@revealui/sync'
|
|
46
|
+
|
|
47
|
+
function MyComponent() {
|
|
48
|
+
const { data: contexts, isLoading } = useAgentContexts()
|
|
49
|
+
|
|
50
|
+
if (isLoading) return <div>Loading...</div>
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<ul>
|
|
54
|
+
{contexts.map(context => (
|
|
55
|
+
<li key={context.id}>{context.name}</li>
|
|
56
|
+
))}
|
|
57
|
+
</ul>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Mutations
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { useAgentContexts } from '@revealui/sync'
|
|
66
|
+
|
|
67
|
+
function CreateContext() {
|
|
68
|
+
const { create } = useAgentContexts()
|
|
69
|
+
|
|
70
|
+
const handleCreate = async () => {
|
|
71
|
+
await create({
|
|
72
|
+
name: 'New Context',
|
|
73
|
+
agent_id: 'agent-123',
|
|
74
|
+
// ... other fields
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return <button onClick={handleCreate}>Create</button>
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Available Hooks
|
|
83
|
+
|
|
84
|
+
### `useAgentContexts()`
|
|
85
|
+
|
|
86
|
+
Sync agent contexts (task context, working memory, etc.)
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const {
|
|
90
|
+
data, // Agent contexts array
|
|
91
|
+
isLoading, // Loading state
|
|
92
|
+
error, // Error state
|
|
93
|
+
create, // Create function
|
|
94
|
+
update, // Update function
|
|
95
|
+
remove // Delete function
|
|
96
|
+
} = useAgentContexts()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `useAgentMemory()`
|
|
100
|
+
|
|
101
|
+
Sync agent memory (episodic, semantic, working)
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
const {
|
|
105
|
+
data, // Memory entries array
|
|
106
|
+
isLoading,
|
|
107
|
+
error,
|
|
108
|
+
create,
|
|
109
|
+
update,
|
|
110
|
+
remove
|
|
111
|
+
} = useAgentMemory()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `useConversations()`
|
|
115
|
+
|
|
116
|
+
Sync conversation history
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const {
|
|
120
|
+
data, // Conversations array
|
|
121
|
+
isLoading,
|
|
122
|
+
error,
|
|
123
|
+
create,
|
|
124
|
+
update,
|
|
125
|
+
remove
|
|
126
|
+
} = useConversations()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## How It Works
|
|
130
|
+
|
|
131
|
+
1. **ElectricSQL Service**: Runs as a sync service between Postgres and clients
|
|
132
|
+
2. **Shape Subscriptions**: Subscribe to "shapes" of data (queries)
|
|
133
|
+
3. **Local Cache**: Data cached locally in browser
|
|
134
|
+
4. **Real-time Updates**: Changes propagate instantly to all connected clients
|
|
135
|
+
5. **Conflict Resolution**: CRDT-based conflict resolution for offline edits
|
|
136
|
+
|
|
137
|
+
## Environment Variables
|
|
138
|
+
|
|
139
|
+
```env
|
|
140
|
+
# ElectricSQL service URL
|
|
141
|
+
NEXT_PUBLIC_ELECTRIC_SERVICE_URL=http://localhost:5133
|
|
142
|
+
|
|
143
|
+
# Optional: Server-side Electric URL (if different)
|
|
144
|
+
ELECTRIC_SERVICE_URL=http://localhost:5133
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# Build package
|
|
151
|
+
pnpm --filter @revealui/sync build
|
|
152
|
+
|
|
153
|
+
# Watch mode
|
|
154
|
+
pnpm --filter @revealui/sync dev
|
|
155
|
+
|
|
156
|
+
# Run tests
|
|
157
|
+
pnpm --filter @revealui/sync test
|
|
158
|
+
|
|
159
|
+
# Type check
|
|
160
|
+
pnpm --filter @revealui/sync typecheck
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Testing
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# Run all tests
|
|
167
|
+
pnpm --filter @revealui/sync test
|
|
168
|
+
|
|
169
|
+
# Watch mode
|
|
170
|
+
pnpm --filter @revealui/sync test:watch
|
|
171
|
+
|
|
172
|
+
# Coverage
|
|
173
|
+
pnpm --filter @revealui/sync test:coverage
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Architecture
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
┌─────────────────┐
|
|
180
|
+
│ React Component │
|
|
181
|
+
│ (useAgentContexts) │
|
|
182
|
+
└────────┬────────┘
|
|
183
|
+
│
|
|
184
|
+
▼
|
|
185
|
+
┌─────────────────┐
|
|
186
|
+
│ ElectricSQL │
|
|
187
|
+
│ Shape Hook │
|
|
188
|
+
└────────┬────────┘
|
|
189
|
+
│
|
|
190
|
+
▼
|
|
191
|
+
┌─────────────────┐
|
|
192
|
+
│ Electric Sync │
|
|
193
|
+
│ Service │
|
|
194
|
+
└────────┬────────┘
|
|
195
|
+
│
|
|
196
|
+
▼
|
|
197
|
+
┌─────────────────┐
|
|
198
|
+
│ PostgreSQL │
|
|
199
|
+
│ Database │
|
|
200
|
+
└─────────────────┘
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Limitations
|
|
204
|
+
|
|
205
|
+
⚠️ **CRITICAL**: ElectricSQL API endpoints need independent verification before production use.
|
|
206
|
+
|
|
207
|
+
**Status:** ⚠️ NEEDS VERIFICATION
|
|
208
|
+
- ElectricSQL integration exists but requires testing
|
|
209
|
+
- API endpoints based on assumptions
|
|
210
|
+
- No integration tests performed yet
|
|
211
|
+
- Not recommended for production until verified
|
|
212
|
+
|
|
213
|
+
See [Project Roadmap](../../docs/PROJECT_ROADMAP.md) and [Production Readiness](../../docs/PRODUCTION_READINESS.md) for details.
|
|
214
|
+
|
|
215
|
+
## Related Documentation
|
|
216
|
+
|
|
217
|
+
- [ElectricSQL Documentation](https://electric-sql.com/docs) - Official ElectricSQL docs
|
|
218
|
+
- [Architecture](../../docs/ARCHITECTURE.md) - System architecture overview
|
|
219
|
+
- [Database Guide](../../docs/DATABASE.md) - Database setup
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
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,CAAA;AAIzE,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;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,55 @@
|
|
|
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
|
+
constructor(options: AgentCollabClientOptions);
|
|
35
|
+
connect(): void;
|
|
36
|
+
disconnect(): void;
|
|
37
|
+
getDocument(): Y.Doc;
|
|
38
|
+
getText(name?: string): Y.Text;
|
|
39
|
+
getTextContent(name?: string): string;
|
|
40
|
+
insertText(index: number, content: string, name?: string): void;
|
|
41
|
+
deleteText(index: number, length: number, name?: string): void;
|
|
42
|
+
replaceAll(content: string, name?: string): void;
|
|
43
|
+
onUpdate(callback: (update: Uint8Array) => void): () => void;
|
|
44
|
+
getConnectedUsers(): Map<number, Record<string, unknown>>;
|
|
45
|
+
waitForSync(timeoutMs?: number): Promise<void>;
|
|
46
|
+
destroy(): void;
|
|
47
|
+
private buildWebSocketUrl;
|
|
48
|
+
private handleOpen;
|
|
49
|
+
private handleMessage;
|
|
50
|
+
private handleClose;
|
|
51
|
+
private handleError;
|
|
52
|
+
private scheduleReconnect;
|
|
53
|
+
private cancelReconnect;
|
|
54
|
+
}
|
|
55
|
+
//# 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,CAAA;AAE5C,OAAO,KAAK,iBAAiB,MAAM,uBAAuB,CAAA;AAE1D,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AAQxB,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,aAAa,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,qBAAa,iBAAkB,SAAQ,UAAU,CAAC,MAAM,CAAC;IACvD,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAA;IACnB,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC,SAAS,CAAA;IAC/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IAExC,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,cAAc,CAA6C;IACnE,OAAO,CAAC,aAAa,CAA+D;IACpF,OAAO,CAAC,sBAAsB,CAGrB;IACT,OAAO,CAAC,SAAS,CAAQ;gBAEb,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;IAMrC,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;IAmB5C,OAAO,IAAI,IAAI;IASf,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,UAAU;IAoClB,OAAO,CAAC,aAAa;IA+BrB,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,eAAe;CAOxB"}
|
|
@@ -0,0 +1,253 @@
|
|
|
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
|
+
const MESSAGE_SYNC = 0;
|
|
9
|
+
const MESSAGE_AWARENESS = 1;
|
|
10
|
+
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
11
|
+
const BASE_RECONNECT_DELAY = 1000;
|
|
12
|
+
const RECONNECT_MULTIPLIER = 2;
|
|
13
|
+
export class AgentCollabClient extends Observable {
|
|
14
|
+
doc;
|
|
15
|
+
awareness;
|
|
16
|
+
serverUrl;
|
|
17
|
+
documentId;
|
|
18
|
+
identity;
|
|
19
|
+
authToken;
|
|
20
|
+
autoReconnect;
|
|
21
|
+
defaultTextName;
|
|
22
|
+
ws = null;
|
|
23
|
+
synced = false;
|
|
24
|
+
reconnectAttempts = 0;
|
|
25
|
+
reconnectTimer = null;
|
|
26
|
+
updateHandler = null;
|
|
27
|
+
awarenessUpdateHandler;
|
|
28
|
+
destroyed = false;
|
|
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
|
+
// Y.Text.toString() returns the text content — ESLint doesn't recognize this
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
88
|
+
return this.getText(name).toString();
|
|
89
|
+
}
|
|
90
|
+
insertText(index, content, name) {
|
|
91
|
+
this.getText(name).insert(index, content);
|
|
92
|
+
}
|
|
93
|
+
deleteText(index, length, name) {
|
|
94
|
+
this.getText(name).delete(index, length);
|
|
95
|
+
}
|
|
96
|
+
replaceAll(content, name) {
|
|
97
|
+
const text = this.getText(name);
|
|
98
|
+
this.doc.transact(() => {
|
|
99
|
+
text.delete(0, text.length);
|
|
100
|
+
text.insert(0, content);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
onUpdate(callback) {
|
|
104
|
+
const handler = (update, origin) => {
|
|
105
|
+
if (origin !== this) {
|
|
106
|
+
callback(update);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
this.doc.on('update', handler);
|
|
110
|
+
return () => this.doc.off('update', handler);
|
|
111
|
+
}
|
|
112
|
+
getConnectedUsers() {
|
|
113
|
+
const users = new Map();
|
|
114
|
+
for (const [clientId, state] of this.awareness.getStates()) {
|
|
115
|
+
if (state && typeof state === 'object') {
|
|
116
|
+
users.set(clientId, state);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return users;
|
|
120
|
+
}
|
|
121
|
+
waitForSync(timeoutMs = 5000) {
|
|
122
|
+
if (this.synced)
|
|
123
|
+
return Promise.resolve();
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
const timeout = setTimeout(() => {
|
|
126
|
+
this.off('sync', handler);
|
|
127
|
+
reject(new Error(`Sync timeout after ${timeoutMs}ms`));
|
|
128
|
+
}, timeoutMs);
|
|
129
|
+
const handler = (isSynced) => {
|
|
130
|
+
if (isSynced) {
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
this.off('sync', handler);
|
|
133
|
+
resolve();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
this.on('sync', handler);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
destroy() {
|
|
140
|
+
this.destroyed = true;
|
|
141
|
+
this.awareness.off('update', this.awarenessUpdateHandler);
|
|
142
|
+
this.disconnect();
|
|
143
|
+
this.awareness.destroy();
|
|
144
|
+
this.doc.destroy();
|
|
145
|
+
super.destroy();
|
|
146
|
+
}
|
|
147
|
+
buildWebSocketUrl() {
|
|
148
|
+
const params = new URLSearchParams({
|
|
149
|
+
name: this.identity.name,
|
|
150
|
+
color: this.identity.color,
|
|
151
|
+
type: 'agent',
|
|
152
|
+
agentModel: this.identity.model,
|
|
153
|
+
});
|
|
154
|
+
if (this.authToken) {
|
|
155
|
+
params.set('token', this.authToken);
|
|
156
|
+
}
|
|
157
|
+
return `${this.serverUrl}/ws/collab/${this.documentId}?${params.toString()}`;
|
|
158
|
+
}
|
|
159
|
+
handleOpen() {
|
|
160
|
+
this.reconnectAttempts = 0;
|
|
161
|
+
this.emit('status', [{ status: 'connected' }]);
|
|
162
|
+
const encoder = encoding.createEncoder();
|
|
163
|
+
encoding.writeVarUint(encoder, MESSAGE_SYNC);
|
|
164
|
+
syncProtocol.writeSyncStep1(encoder, this.doc);
|
|
165
|
+
this.ws?.send(encoding.toUint8Array(encoder));
|
|
166
|
+
this.awareness.setLocalState({
|
|
167
|
+
name: this.identity.name,
|
|
168
|
+
color: this.identity.color,
|
|
169
|
+
type: 'agent',
|
|
170
|
+
agentModel: this.identity.model,
|
|
171
|
+
});
|
|
172
|
+
const awarenessUpdate = awarenessProtocol.encodeAwarenessUpdate(this.awareness, [
|
|
173
|
+
this.doc.clientID,
|
|
174
|
+
]);
|
|
175
|
+
const awarenessEncoder = encoding.createEncoder();
|
|
176
|
+
encoding.writeVarUint(awarenessEncoder, MESSAGE_AWARENESS);
|
|
177
|
+
encoding.writeVarUint8Array(awarenessEncoder, awarenessUpdate);
|
|
178
|
+
this.ws?.send(encoding.toUint8Array(awarenessEncoder));
|
|
179
|
+
this.updateHandler = (update, origin) => {
|
|
180
|
+
if (origin === this)
|
|
181
|
+
return;
|
|
182
|
+
const updateEncoder = encoding.createEncoder();
|
|
183
|
+
encoding.writeVarUint(updateEncoder, MESSAGE_SYNC);
|
|
184
|
+
syncProtocol.writeUpdate(updateEncoder, update);
|
|
185
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
186
|
+
this.ws.send(encoding.toUint8Array(updateEncoder));
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
this.doc.on('update', this.updateHandler);
|
|
190
|
+
}
|
|
191
|
+
handleMessage(data) {
|
|
192
|
+
const uint8 = data instanceof ArrayBuffer
|
|
193
|
+
? new Uint8Array(data)
|
|
194
|
+
: Array.isArray(data)
|
|
195
|
+
? new Uint8Array(Buffer.concat(data))
|
|
196
|
+
: new Uint8Array(data);
|
|
197
|
+
const decoder = decoding.createDecoder(uint8);
|
|
198
|
+
const messageType = decoding.readVarUint(decoder);
|
|
199
|
+
if (messageType === MESSAGE_SYNC) {
|
|
200
|
+
const responseEncoder = encoding.createEncoder();
|
|
201
|
+
encoding.writeVarUint(responseEncoder, MESSAGE_SYNC);
|
|
202
|
+
const syncMessageType = syncProtocol.readSyncMessage(decoder, responseEncoder, this.doc, this);
|
|
203
|
+
if (encoding.length(responseEncoder) > 1) {
|
|
204
|
+
this.ws?.send(encoding.toUint8Array(responseEncoder));
|
|
205
|
+
}
|
|
206
|
+
if (syncMessageType === 1 && !this.synced) {
|
|
207
|
+
this.synced = true;
|
|
208
|
+
}
|
|
209
|
+
this.emit('sync', [true]);
|
|
210
|
+
}
|
|
211
|
+
else if (messageType === MESSAGE_AWARENESS) {
|
|
212
|
+
const update = decoding.readVarUint8Array(decoder);
|
|
213
|
+
awarenessProtocol.applyAwarenessUpdate(this.awareness, update, 'remote');
|
|
214
|
+
this.emit('awareness', [this.getConnectedUsers()]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
handleClose() {
|
|
218
|
+
if (this.updateHandler) {
|
|
219
|
+
this.doc.off('update', this.updateHandler);
|
|
220
|
+
this.updateHandler = null;
|
|
221
|
+
}
|
|
222
|
+
this.ws = null;
|
|
223
|
+
this.synced = false;
|
|
224
|
+
this.emit('sync', [false]);
|
|
225
|
+
this.emit('status', [{ status: 'disconnected' }]);
|
|
226
|
+
if (this.autoReconnect && !this.destroyed) {
|
|
227
|
+
this.scheduleReconnect();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
handleError(err) {
|
|
231
|
+
this.emit('error', [err]);
|
|
232
|
+
this.ws?.close();
|
|
233
|
+
}
|
|
234
|
+
scheduleReconnect() {
|
|
235
|
+
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
236
|
+
this.emit('status', [{ status: 'failed' }]);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const delay = BASE_RECONNECT_DELAY * RECONNECT_MULTIPLIER ** this.reconnectAttempts;
|
|
240
|
+
this.reconnectAttempts++;
|
|
241
|
+
this.reconnectTimer = setTimeout(() => {
|
|
242
|
+
this.reconnectTimer = null;
|
|
243
|
+
this.connect();
|
|
244
|
+
}, delay);
|
|
245
|
+
}
|
|
246
|
+
cancelReconnect() {
|
|
247
|
+
if (this.reconnectTimer) {
|
|
248
|
+
clearTimeout(this.reconnectTimer);
|
|
249
|
+
this.reconnectTimer = null;
|
|
250
|
+
}
|
|
251
|
+
this.reconnectAttempts = 0;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { CollabDocumentState } from './use-collab-document.js';
|
|
2
|
+
export { useCollabDocument } from './use-collab-document.js';
|
|
3
|
+
export type { CollaborationIdentity, UseCollaborationOptions, UseCollaborationResult, } from './use-collaboration.js';
|
|
4
|
+
export { useCollaboration } from './use-collaboration.js';
|
|
5
|
+
export type { UserPresence } from './yjs-websocket-provider.js';
|
|
6
|
+
export { CollabProvider } from './yjs-websocket-provider.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/collab/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAC5D,YAAY,EACV,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
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
|
+
//# 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,CAAA;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,YAAY,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AACzE,OAAO,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAA"}
|
|
@@ -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, electricUrl: 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":"AAEA,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,UAAU,GAAG,IAAI,CAAA;IAC/B,gBAAgB,EAAE,MAAM,CAAA;IACxB,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAKD,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,mBAAmB,CA+C9F"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useShape } from '@electric-sql/react';
|
|
2
|
+
// UUID v4 pattern — only format accepted as a yjs_documents PK
|
|
3
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
export function useCollabDocument(documentId, electricUrl) {
|
|
5
|
+
// Validate before interpolating into the WHERE clause. yjs_documents PKs are
|
|
6
|
+
// always UUIDs — reject anything else so no untrusted string enters the query.
|
|
7
|
+
const isValidId = UUID_RE.test(documentId);
|
|
8
|
+
// Hook must always be called (Rules of Hooks). Pass an impossible WHERE when
|
|
9
|
+
// the ID is invalid so the shape returns no rows but the hook still runs.
|
|
10
|
+
const { data, isLoading, error } = useShape({
|
|
11
|
+
url: `${electricUrl}/v1/shape`,
|
|
12
|
+
params: {
|
|
13
|
+
table: 'yjs_documents',
|
|
14
|
+
where: isValidId ? `id = '${documentId}'` : `id = 'invalid'`,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
if (!isValidId) {
|
|
18
|
+
return {
|
|
19
|
+
initialState: null,
|
|
20
|
+
connectedClients: 0,
|
|
21
|
+
isLoading: false,
|
|
22
|
+
error: new Error('Invalid documentId: must be a UUID'),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
let initialState = null;
|
|
26
|
+
let connectedClients = 0;
|
|
27
|
+
if (data && data.length > 0) {
|
|
28
|
+
const row = data[0];
|
|
29
|
+
const state = row.state;
|
|
30
|
+
if (state) {
|
|
31
|
+
const binary = atob(state);
|
|
32
|
+
const bytes = new Uint8Array(binary.length);
|
|
33
|
+
for (let i = 0; i < binary.length; i++) {
|
|
34
|
+
bytes[i] = binary.charCodeAt(i);
|
|
35
|
+
}
|
|
36
|
+
initialState = bytes;
|
|
37
|
+
}
|
|
38
|
+
connectedClients = row.connected_clients ?? 0;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
initialState,
|
|
42
|
+
connectedClients,
|
|
43
|
+
isLoading,
|
|
44
|
+
error: error ? new Error(String(error)) : null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -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,CAAA;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAE5D,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,YAAY,CAAC,EAAE,UAAU,GAAG,IAAI,CAAA;IAChC,QAAQ,CAAC,EAAE,qBAAqB,CAAA;CACjC;AAED,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,CAAC,CAAC,GAAG,GAAG,IAAI,CAAA;IACjB,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAA;IAC/B,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;CAC1C;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,CAAA;AAC5C,OAAO,KAAK,iBAAiB,MAAM,uBAAuB,CAAA;AAE1D,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AAQxB,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CAC3C;AAED,qBAAa,cAAe,SAAQ,UAAU,CAAC,MAAM,CAAC;IACpD,GAAG,EAAE,CAAC,CAAC,GAAG,CAAA;IACV,SAAS,EAAE,iBAAiB,CAAC,SAAS,CAAA;IACtC,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,cAAc,CAA6C;IACnE,OAAO,CAAC,aAAa,CAA+D;IACpF,OAAO,CAAC,sBAAsB,CAGrB;gBAGP,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,173 @@
|
|
|
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
|
+
const MESSAGE_SYNC = 0;
|
|
8
|
+
const MESSAGE_AWARENESS = 1;
|
|
9
|
+
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
10
|
+
const BASE_RECONNECT_DELAY = 1000;
|
|
11
|
+
const RECONNECT_MULTIPLIER = 1.5;
|
|
12
|
+
export class CollabProvider extends Observable {
|
|
13
|
+
doc;
|
|
14
|
+
awareness;
|
|
15
|
+
serverUrl;
|
|
16
|
+
documentId;
|
|
17
|
+
ws = null;
|
|
18
|
+
synced = false;
|
|
19
|
+
reconnectAttempts = 0;
|
|
20
|
+
reconnectTimer = null;
|
|
21
|
+
updateHandler = null;
|
|
22
|
+
awarenessUpdateHandler;
|
|
23
|
+
constructor(serverUrl, documentId, doc, options) {
|
|
24
|
+
super();
|
|
25
|
+
this.serverUrl = serverUrl;
|
|
26
|
+
this.documentId = documentId;
|
|
27
|
+
this.doc = doc;
|
|
28
|
+
this.awareness = new awarenessProtocol.Awareness(doc);
|
|
29
|
+
if (options?.initialState) {
|
|
30
|
+
Y.applyUpdate(doc, options.initialState);
|
|
31
|
+
}
|
|
32
|
+
// Send awareness updates over WebSocket
|
|
33
|
+
this.awarenessUpdateHandler = ({ added, updated, removed }, origin) => {
|
|
34
|
+
if (origin === 'remote')
|
|
35
|
+
return;
|
|
36
|
+
const changedClients = added.concat(updated, removed);
|
|
37
|
+
const update = awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients);
|
|
38
|
+
const encoder = encoding.createEncoder();
|
|
39
|
+
encoding.writeVarUint(encoder, MESSAGE_AWARENESS);
|
|
40
|
+
encoding.writeVarUint8Array(encoder, update);
|
|
41
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
42
|
+
this.ws.send(encoding.toUint8Array(encoder));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
this.awareness.on('update', this.awarenessUpdateHandler);
|
|
46
|
+
}
|
|
47
|
+
setLocalIdentity(identity) {
|
|
48
|
+
this.awareness.setLocalState(identity);
|
|
49
|
+
}
|
|
50
|
+
getConnectedUsers() {
|
|
51
|
+
const users = new Map();
|
|
52
|
+
for (const [clientId, state] of this.awareness.getStates()) {
|
|
53
|
+
if (state && typeof state === 'object') {
|
|
54
|
+
users.set(clientId, state);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return users;
|
|
58
|
+
}
|
|
59
|
+
connect() {
|
|
60
|
+
if (this.ws)
|
|
61
|
+
return;
|
|
62
|
+
const url = `${this.serverUrl}/ws/collab/${this.documentId}`;
|
|
63
|
+
this.ws = new WebSocket(url);
|
|
64
|
+
this.ws.binaryType = 'arraybuffer';
|
|
65
|
+
this.emit('status', [{ status: 'connecting' }]);
|
|
66
|
+
this.ws.onopen = () => {
|
|
67
|
+
this.reconnectAttempts = 0;
|
|
68
|
+
this.emit('status', [{ status: 'connected' }]);
|
|
69
|
+
const encoder = encoding.createEncoder();
|
|
70
|
+
encoding.writeVarUint(encoder, MESSAGE_SYNC);
|
|
71
|
+
syncProtocol.writeSyncStep1(encoder, this.doc);
|
|
72
|
+
this.ws?.send(encoding.toUint8Array(encoder));
|
|
73
|
+
// Send local awareness state to server
|
|
74
|
+
const localState = this.awareness.getLocalState();
|
|
75
|
+
if (localState) {
|
|
76
|
+
const awarenessUpdate = awarenessProtocol.encodeAwarenessUpdate(this.awareness, [
|
|
77
|
+
this.doc.clientID,
|
|
78
|
+
]);
|
|
79
|
+
const awarenessEncoder = encoding.createEncoder();
|
|
80
|
+
encoding.writeVarUint(awarenessEncoder, MESSAGE_AWARENESS);
|
|
81
|
+
encoding.writeVarUint8Array(awarenessEncoder, awarenessUpdate);
|
|
82
|
+
this.ws?.send(encoding.toUint8Array(awarenessEncoder));
|
|
83
|
+
}
|
|
84
|
+
this.updateHandler = (update, origin) => {
|
|
85
|
+
if (origin === this)
|
|
86
|
+
return;
|
|
87
|
+
const updateEncoder = encoding.createEncoder();
|
|
88
|
+
encoding.writeVarUint(updateEncoder, MESSAGE_SYNC);
|
|
89
|
+
syncProtocol.writeUpdate(updateEncoder, update);
|
|
90
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
91
|
+
this.ws.send(encoding.toUint8Array(updateEncoder));
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
this.doc.on('update', this.updateHandler);
|
|
95
|
+
};
|
|
96
|
+
this.ws.onmessage = (event) => {
|
|
97
|
+
const data = new Uint8Array(event.data);
|
|
98
|
+
const decoder = decoding.createDecoder(data);
|
|
99
|
+
const messageType = decoding.readVarUint(decoder);
|
|
100
|
+
if (messageType === MESSAGE_SYNC) {
|
|
101
|
+
const responseEncoder = encoding.createEncoder();
|
|
102
|
+
encoding.writeVarUint(responseEncoder, MESSAGE_SYNC);
|
|
103
|
+
const syncMessageType = syncProtocol.readSyncMessage(decoder, responseEncoder, this.doc, this);
|
|
104
|
+
if (encoding.length(responseEncoder) > 1) {
|
|
105
|
+
this.ws?.send(encoding.toUint8Array(responseEncoder));
|
|
106
|
+
}
|
|
107
|
+
if (syncMessageType === 1 && !this.synced)
|
|
108
|
+
this.synced = true;
|
|
109
|
+
this.emit('sync', [true]);
|
|
110
|
+
}
|
|
111
|
+
else if (messageType === MESSAGE_AWARENESS) {
|
|
112
|
+
const update = decoding.readVarUint8Array(decoder);
|
|
113
|
+
awarenessProtocol.applyAwarenessUpdate(this.awareness, update, 'remote');
|
|
114
|
+
this.emit('awareness', [this.getConnectedUsers()]);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
this.ws.onclose = () => {
|
|
118
|
+
if (this.updateHandler) {
|
|
119
|
+
this.doc.off('update', this.updateHandler);
|
|
120
|
+
this.updateHandler = null;
|
|
121
|
+
}
|
|
122
|
+
this.ws = null;
|
|
123
|
+
this.synced = false;
|
|
124
|
+
this.emit('sync', [false]);
|
|
125
|
+
this.emit('status', [{ status: 'disconnected' }]);
|
|
126
|
+
this.scheduleReconnect();
|
|
127
|
+
};
|
|
128
|
+
this.ws.onerror = () => {
|
|
129
|
+
this.ws?.close();
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
disconnect() {
|
|
133
|
+
this.cancelReconnect();
|
|
134
|
+
if (this.updateHandler) {
|
|
135
|
+
this.doc.off('update', this.updateHandler);
|
|
136
|
+
this.updateHandler = null;
|
|
137
|
+
}
|
|
138
|
+
// Broadcast awareness removal before disconnecting
|
|
139
|
+
awarenessProtocol.removeAwarenessStates(this.awareness, [this.doc.clientID], 'local');
|
|
140
|
+
if (this.ws) {
|
|
141
|
+
this.ws.onclose = null;
|
|
142
|
+
this.ws.close();
|
|
143
|
+
this.ws = null;
|
|
144
|
+
}
|
|
145
|
+
this.synced = false;
|
|
146
|
+
this.emit('status', [{ status: 'disconnected' }]);
|
|
147
|
+
}
|
|
148
|
+
scheduleReconnect() {
|
|
149
|
+
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
150
|
+
this.emit('status', [{ status: 'failed' }]);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const delay = BASE_RECONNECT_DELAY * RECONNECT_MULTIPLIER ** this.reconnectAttempts;
|
|
154
|
+
this.reconnectAttempts++;
|
|
155
|
+
this.reconnectTimer = setTimeout(() => {
|
|
156
|
+
this.reconnectTimer = null;
|
|
157
|
+
this.connect();
|
|
158
|
+
}, delay);
|
|
159
|
+
}
|
|
160
|
+
cancelReconnect() {
|
|
161
|
+
if (this.reconnectTimer) {
|
|
162
|
+
clearTimeout(this.reconnectTimer);
|
|
163
|
+
this.reconnectTimer = null;
|
|
164
|
+
}
|
|
165
|
+
this.reconnectAttempts = 0;
|
|
166
|
+
}
|
|
167
|
+
destroy() {
|
|
168
|
+
this.awareness.off('update', this.awarenessUpdateHandler);
|
|
169
|
+
this.disconnect();
|
|
170
|
+
this.awareness.destroy();
|
|
171
|
+
super.destroy();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type ConversationRecord = {
|
|
2
|
+
id: string | number;
|
|
3
|
+
title?: string | null;
|
|
4
|
+
};
|
|
5
|
+
export declare function useConversations(_userId: string): {
|
|
6
|
+
conversations: ConversationRecord[];
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
error: Error | null;
|
|
9
|
+
};
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=useConversations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useConversations.d.ts","sourceRoot":"","sources":["../../src/hooks/useConversations.ts"],"names":[],"mappings":"AAIA,KAAK,kBAAkB,GAAG;IACxB,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB,CAAA;AAID,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM;;;WAU5B,KAAK,GAAG,IAAI;EAE/B"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useShape } from '@electric-sql/react';
|
|
3
|
+
// _userId kept for API compatibility — filtering is enforced by the server-side
|
|
4
|
+
// proxy at /api/shapes/conversations, which reads the session cookie directly.
|
|
5
|
+
export function useConversations(_userId) {
|
|
6
|
+
// The proxy validates the session and enforces row-level filtering server-side.
|
|
7
|
+
// Client-provided params are not forwarded — the proxy overrides them.
|
|
8
|
+
const { data, isLoading, error } = useShape({
|
|
9
|
+
url: `/api/shapes/conversations`,
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
conversations: Array.isArray(data) ? data : [],
|
|
13
|
+
isLoading,
|
|
14
|
+
error: error,
|
|
15
|
+
};
|
|
16
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { CollabDocumentState, UseCollaborationOptions, UseCollaborationResult, } from './collab/index.js';
|
|
2
|
+
export { CollabProvider, useCollabDocument, useCollaboration, } from './collab/index.js';
|
|
3
|
+
export { useConversations } from './hooks/useConversations.js';
|
|
4
|
+
export { ElectricProvider } from './provider/index.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/provider/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,SAAS,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,2CAIA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
// ElectricProvider placeholder - useShape works with proxy API
|
|
4
|
+
export function ElectricProvider({ children, }) {
|
|
5
|
+
// For now, just pass through children
|
|
6
|
+
// useShape hooks work directly with proxy API endpoints
|
|
7
|
+
return _jsx(_Fragment, { children: children });
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-setup.d.ts","sourceRoot":"","sources":["../src/test-setup.ts"],"names":[],"mappings":"AAAA,OAAO,kCAAkC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/vitest';
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@revealui/sync",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"description": "ElectricSQL sync utilities for RevealUI",
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@electric-sql/react": "^1.0.27",
|
|
11
|
+
"lib0": "^0.2.117",
|
|
12
|
+
"ws": "^8.18.0",
|
|
13
|
+
"y-protocols": "^1.0.7",
|
|
14
|
+
"yjs": "^13.6.29",
|
|
15
|
+
"@revealui/contracts": "1.0.0",
|
|
16
|
+
"@revealui/db": "0.2.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@testing-library/jest-dom": "^6.6.4",
|
|
20
|
+
"@types/ws": "^8.5.14",
|
|
21
|
+
"@testing-library/react": "^16.3.0",
|
|
22
|
+
"@testing-library/react-hooks": "^8.0.1",
|
|
23
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
24
|
+
"jsdom": "^27.4.0",
|
|
25
|
+
"react": "^19.2.3",
|
|
26
|
+
"react-dom": "^19.2.3",
|
|
27
|
+
"typescript": "^5.9.3",
|
|
28
|
+
"vitest": "^4.0.18",
|
|
29
|
+
"dev": "0.0.1"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./provider": {
|
|
37
|
+
"types": "./dist/provider/index.d.ts",
|
|
38
|
+
"import": "./dist/provider/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./collab": {
|
|
41
|
+
"types": "./dist/collab/index.d.ts",
|
|
42
|
+
"import": "./dist/collab/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./collab/server": {
|
|
45
|
+
"types": "./dist/collab/server-index.d.ts",
|
|
46
|
+
"import": "./dist/collab/server-index.js"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"main": "./dist/index.js",
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public",
|
|
52
|
+
"registry": "https://registry.npmjs.org"
|
|
53
|
+
},
|
|
54
|
+
"type": "module",
|
|
55
|
+
"types": "./dist/index.d.ts",
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc",
|
|
58
|
+
"clean": "rm -rf dist",
|
|
59
|
+
"dev": "tsc --watch",
|
|
60
|
+
"lint": "biome check .",
|
|
61
|
+
"lint:eslint": "eslint .",
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"test:coverage": "vitest run --coverage",
|
|
64
|
+
"test:watch": "vitest",
|
|
65
|
+
"typecheck": "tsc --noEmit"
|
|
66
|
+
}
|
|
67
|
+
}
|