@electric-sql/y-electric 0.1.36 → 0.1.38
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/bin/intent.mjs +6 -0
- package/package.json +10 -3
- package/skills/electric-yjs/SKILL.md +268 -0
package/bin/intent.mjs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Auto-generated by @tanstack/intent setup
|
|
3
|
+
// Exposes the intent end-user CLI for consumers of this library.
|
|
4
|
+
// Commit this file, then add to your package.json:
|
|
5
|
+
// "bin": { "intent": "./bin/intent.mjs" }
|
|
6
|
+
await import(`@tanstack/intent/intent-library`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-sql/y-electric",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"description": "YJS network provider for ElectricSQL",
|
|
5
5
|
"author": "ElectricSQL team and contributors.",
|
|
6
6
|
"bugs": {
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
"lib0": "^0.2.65",
|
|
12
12
|
"y-protocols": "^1.0.5",
|
|
13
13
|
"yjs": "^13.6.6",
|
|
14
|
-
"@electric-sql/client": "1.5.
|
|
14
|
+
"@electric-sql/client": "1.5.12"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
+
"@tanstack/intent": "^0.0.9",
|
|
17
18
|
"@types/node": "^22.0.0",
|
|
18
19
|
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
|
19
20
|
"@typescript-eslint/parser": "^7.14.1",
|
|
@@ -47,9 +48,15 @@
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
},
|
|
51
|
+
"bin": {
|
|
52
|
+
"intent": "./bin/intent.mjs"
|
|
53
|
+
},
|
|
50
54
|
"files": [
|
|
51
55
|
"dist",
|
|
52
|
-
"src"
|
|
56
|
+
"src",
|
|
57
|
+
"skills",
|
|
58
|
+
"bin",
|
|
59
|
+
"!skills/_artifacts"
|
|
53
60
|
],
|
|
54
61
|
"homepage": "https://electric-sql.com",
|
|
55
62
|
"license": "Apache-2.0",
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: electric-yjs
|
|
3
|
+
description: >
|
|
4
|
+
Set up ElectricProvider for real-time collaborative editing with Yjs via
|
|
5
|
+
Electric shapes. Covers ElectricProvider configuration, document updates
|
|
6
|
+
shape with BYTEA parser (parseToDecoder), awareness shape at offset='now',
|
|
7
|
+
LocalStorageResumeStateProvider for reconnection with stableStateVector
|
|
8
|
+
diff, debounceMs for batching writes, sendUrl PUT endpoint, required
|
|
9
|
+
Postgres schema (ydoc_update and ydoc_awareness tables), CORS header
|
|
10
|
+
exposure, and sendErrorRetryHandler. Load when implementing collaborative
|
|
11
|
+
editing with Yjs and Electric.
|
|
12
|
+
type: composition
|
|
13
|
+
library: electric
|
|
14
|
+
library_version: '0.1.36'
|
|
15
|
+
requires:
|
|
16
|
+
- electric-shapes
|
|
17
|
+
sources:
|
|
18
|
+
- 'electric-sql/electric:packages/y-electric/src/y-electric.ts'
|
|
19
|
+
- 'electric-sql/electric:packages/y-electric/src/types.ts'
|
|
20
|
+
- 'electric-sql/electric:packages/y-electric/src/local-storage-resume-state.ts'
|
|
21
|
+
- 'electric-sql/electric:packages/y-electric/src/utils.ts'
|
|
22
|
+
- 'electric-sql/electric:examples/yjs/'
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This skill builds on electric-shapes. Read it first for ShapeStream configuration.
|
|
26
|
+
|
|
27
|
+
# Electric — Yjs Collaboration
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
30
|
+
|
|
31
|
+
### 1. Create Postgres tables
|
|
32
|
+
|
|
33
|
+
```sql
|
|
34
|
+
CREATE TABLE ydoc_update (
|
|
35
|
+
id SERIAL PRIMARY KEY,
|
|
36
|
+
room TEXT NOT NULL,
|
|
37
|
+
update BYTEA NOT NULL
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE TABLE ydoc_awareness (
|
|
41
|
+
client_id TEXT,
|
|
42
|
+
room TEXT,
|
|
43
|
+
update BYTEA NOT NULL,
|
|
44
|
+
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
45
|
+
PRIMARY KEY (client_id, room)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
-- Garbage collect stale awareness entries
|
|
49
|
+
CREATE OR REPLACE FUNCTION gc_awareness_timeouts()
|
|
50
|
+
RETURNS TRIGGER AS $$
|
|
51
|
+
BEGIN
|
|
52
|
+
DELETE FROM ydoc_awareness
|
|
53
|
+
WHERE updated_at < (CURRENT_TIMESTAMP - INTERVAL '30 seconds')
|
|
54
|
+
AND room = NEW.room;
|
|
55
|
+
RETURN NEW;
|
|
56
|
+
END;
|
|
57
|
+
$$ LANGUAGE plpgsql;
|
|
58
|
+
|
|
59
|
+
CREATE TRIGGER gc_awareness
|
|
60
|
+
AFTER INSERT OR UPDATE ON ydoc_awareness
|
|
61
|
+
FOR EACH ROW EXECUTE FUNCTION gc_awareness_timeouts();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Create server endpoint for receiving updates
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// PUT /api/yjs/update — receives binary Yjs update
|
|
68
|
+
app.put('/api/yjs/update', async (req, res) => {
|
|
69
|
+
const body = Buffer.from(await req.arrayBuffer())
|
|
70
|
+
await db.query('INSERT INTO ydoc_update (room, update) VALUES ($1, $2)', [
|
|
71
|
+
req.headers['x-room-id'],
|
|
72
|
+
body,
|
|
73
|
+
])
|
|
74
|
+
res.status(200).end()
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. Configure ElectricProvider
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import * as Y from 'yjs'
|
|
82
|
+
import {
|
|
83
|
+
ElectricProvider,
|
|
84
|
+
LocalStorageResumeStateProvider,
|
|
85
|
+
parseToDecoder,
|
|
86
|
+
} from '@electric-sql/y-electric'
|
|
87
|
+
|
|
88
|
+
const ydoc = new Y.Doc()
|
|
89
|
+
const roomId = 'my-document'
|
|
90
|
+
|
|
91
|
+
const resumeProvider = new LocalStorageResumeStateProvider(roomId)
|
|
92
|
+
|
|
93
|
+
const provider = new ElectricProvider({
|
|
94
|
+
doc: ydoc,
|
|
95
|
+
documentUpdates: {
|
|
96
|
+
shape: {
|
|
97
|
+
url: `/api/yjs/doc-shape?room=${roomId}`,
|
|
98
|
+
parser: parseToDecoder,
|
|
99
|
+
},
|
|
100
|
+
sendUrl: '/api/yjs/update',
|
|
101
|
+
getUpdateFromRow: (row) => row.update,
|
|
102
|
+
},
|
|
103
|
+
awarenessUpdates: {
|
|
104
|
+
shape: {
|
|
105
|
+
url: `/api/yjs/awareness-shape?room=${roomId}`,
|
|
106
|
+
parser: parseToDecoder,
|
|
107
|
+
offset: 'now', // Only live awareness, no historical backfill
|
|
108
|
+
},
|
|
109
|
+
sendUrl: '/api/yjs/awareness',
|
|
110
|
+
protocol: provider.awareness,
|
|
111
|
+
getUpdateFromRow: (row) => row.update,
|
|
112
|
+
},
|
|
113
|
+
resumeState: resumeProvider.load(),
|
|
114
|
+
debounceMs: 100, // Batch rapid edits
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Persist resume state for efficient reconnection
|
|
118
|
+
resumeProvider.subscribeToResumeState(provider)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Core Patterns
|
|
122
|
+
|
|
123
|
+
### CORS headers for Yjs proxy
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// Proxy must expose Electric headers
|
|
127
|
+
const corsHeaders = {
|
|
128
|
+
'Access-Control-Expose-Headers':
|
|
129
|
+
'electric-offset, electric-handle, electric-schema, electric-cursor',
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Resume state for reconnection
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// On construction, pass stored resume state
|
|
137
|
+
const provider = new ElectricProvider({
|
|
138
|
+
doc: ydoc,
|
|
139
|
+
documentUpdates: { shape: shapeOpts, sendUrl: '/api/yjs/update' },
|
|
140
|
+
resumeState: resumeProvider.load(),
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// Subscribe to persist updates
|
|
144
|
+
const unsub = resumeProvider.subscribeToResumeState(provider)
|
|
145
|
+
|
|
146
|
+
// Clean up
|
|
147
|
+
provider.destroy()
|
|
148
|
+
unsub()
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
When `stableStateVector` is provided in resume state, the provider sends only the diff between the stored vector and current doc state on reconnect.
|
|
152
|
+
|
|
153
|
+
### Connection lifecycle
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
provider.on('status', ({ status }) => {
|
|
157
|
+
// 'connecting' | 'connected' | 'disconnected'
|
|
158
|
+
console.log('Yjs sync status:', status)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
provider.on('sync', (synced: boolean) => {
|
|
162
|
+
console.log('Document synced:', synced)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Manual disconnect/reconnect
|
|
166
|
+
provider.disconnect()
|
|
167
|
+
provider.connect()
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Common Mistakes
|
|
171
|
+
|
|
172
|
+
### HIGH Not persisting resume state for reconnection
|
|
173
|
+
|
|
174
|
+
Wrong:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const provider = new ElectricProvider({
|
|
178
|
+
doc: ydoc,
|
|
179
|
+
documentUpdates: {
|
|
180
|
+
shape: { url: '/api/yjs/doc-shape', parser: parseToDecoder },
|
|
181
|
+
sendUrl: '/api/yjs/update',
|
|
182
|
+
getUpdateFromRow: (row) => row.update,
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Correct:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const resumeProvider = new LocalStorageResumeStateProvider('my-doc')
|
|
191
|
+
const provider = new ElectricProvider({
|
|
192
|
+
doc: ydoc,
|
|
193
|
+
documentUpdates: {
|
|
194
|
+
shape: { url: '/api/yjs/doc-shape', parser: parseToDecoder },
|
|
195
|
+
sendUrl: '/api/yjs/update',
|
|
196
|
+
getUpdateFromRow: (row) => row.update,
|
|
197
|
+
},
|
|
198
|
+
resumeState: resumeProvider.load(),
|
|
199
|
+
})
|
|
200
|
+
resumeProvider.subscribeToResumeState(provider)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Without `resumeState`, the provider fetches the ENTIRE document shape on every reconnect. With `stableStateVector`, only a diff is sent.
|
|
204
|
+
|
|
205
|
+
Source: `packages/y-electric/src/types.ts:102-112`
|
|
206
|
+
|
|
207
|
+
### HIGH Missing BYTEA parser for shape streams
|
|
208
|
+
|
|
209
|
+
Wrong:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
documentUpdates: {
|
|
213
|
+
shape: { url: '/api/yjs/doc-shape' },
|
|
214
|
+
sendUrl: '/api/yjs/update',
|
|
215
|
+
getUpdateFromRow: (row) => row.update,
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Correct:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { parseToDecoder } from '@electric-sql/y-electric'
|
|
223
|
+
|
|
224
|
+
documentUpdates: {
|
|
225
|
+
shape: {
|
|
226
|
+
url: '/api/yjs/doc-shape',
|
|
227
|
+
parser: parseToDecoder,
|
|
228
|
+
},
|
|
229
|
+
sendUrl: '/api/yjs/update',
|
|
230
|
+
getUpdateFromRow: (row) => row.update,
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Yjs updates are stored as BYTEA in Postgres. Without `parseToDecoder`, the shape returns raw hex strings instead of lib0 Decoders, and `Y.applyUpdate` fails silently or corrupts the document.
|
|
235
|
+
|
|
236
|
+
Source: `packages/y-electric/src/utils.ts`
|
|
237
|
+
|
|
238
|
+
### MEDIUM Not setting debounceMs for collaborative editing
|
|
239
|
+
|
|
240
|
+
Wrong:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
const provider = new ElectricProvider({
|
|
244
|
+
doc: ydoc,
|
|
245
|
+
documentUpdates: { shape: shapeOpts, sendUrl: '/api/yjs/update' },
|
|
246
|
+
// Default debounceMs = 0: every keystroke sends a PUT
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Correct:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
const provider = new ElectricProvider({
|
|
254
|
+
doc: ydoc,
|
|
255
|
+
documentUpdates: { shape: shapeOpts, sendUrl: '/api/yjs/update' },
|
|
256
|
+
debounceMs: 100,
|
|
257
|
+
})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Default `debounceMs` is 0, sending a PUT request for every keystroke. Set to 100+ to batch rapid edits and reduce server load.
|
|
261
|
+
|
|
262
|
+
Source: `packages/y-electric/src/y-electric.ts`
|
|
263
|
+
|
|
264
|
+
See also: electric-shapes/SKILL.md — Shape configuration and parser setup.
|
|
265
|
+
|
|
266
|
+
## Version
|
|
267
|
+
|
|
268
|
+
Targets @electric-sql/y-electric v0.1.x.
|