@electric-sql/y-electric 0.1.21 → 0.1.23

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/src/y-electric.ts CHANGED
@@ -56,6 +56,8 @@ export class ElectricProvider<
56
56
  private pendingChanges: Uint8Array | null = null
57
57
  private sendingAwarenessState: boolean = false
58
58
  private pendingAwarenessUpdate: AwarenessUpdate | null = null
59
+ private debounceMs: number
60
+ private debounceTimer: ReturnType<typeof setTimeout> | null = null
59
61
 
60
62
  private documentUpdateHandler: (
61
63
  update: Uint8Array,
@@ -93,6 +95,7 @@ export class ElectricProvider<
93
95
  * @param {ResumeState} [options.resumeState] - Resume state for the provider
94
96
  * @param {boolean} [options.connect=true] - Whether to automatically connect upon initialization
95
97
  * @param {typeof fetch} [options.fetchClient] - Custom fetch implementation to use for HTTP requests
98
+ * @param {number} [options.debounceMs] - Debounce window in milliseconds for sending document updates. If 0 or undefined, debouncing is disabled.
96
99
  */
97
100
  constructor({
98
101
  doc,
@@ -101,6 +104,7 @@ export class ElectricProvider<
101
104
  resumeState,
102
105
  connect = true,
103
106
  fetchClient,
107
+ debounceMs,
104
108
  }: ElectricProviderOptions<RowWithDocumentUpdate, RowWithAwarenessUpdate>) {
105
109
  super()
106
110
 
@@ -108,6 +112,7 @@ export class ElectricProvider<
108
112
  this.documentUpdates = documentUpdatesConfig
109
113
  this.awarenessUpdates = awarenessUpdatesConfig
110
114
  this.resumeState = resumeState ?? {}
115
+ this.debounceMs = debounceMs ?? 0
111
116
 
112
117
  this.fetchClient = fetchClient
113
118
 
@@ -174,7 +179,35 @@ export class ElectricProvider<
174
179
  }
175
180
  }
176
181
 
182
+ private clearDebounceTimer() {
183
+ if (this.debounceTimer !== null) {
184
+ clearTimeout(this.debounceTimer)
185
+ this.debounceTimer = null
186
+ }
187
+ }
188
+
189
+ private scheduleSendOperations() {
190
+ if (this.debounceMs > 0) {
191
+ if (this.debounceTimer === null) {
192
+ this.debounceTimer = setTimeout(async () => {
193
+ this.debounceTimer = null
194
+ await this.sendOperations()
195
+ if (
196
+ this.pendingChanges &&
197
+ this.connected &&
198
+ !this.sendingPendingChanges
199
+ ) {
200
+ this.scheduleSendOperations()
201
+ }
202
+ }, this.debounceMs)
203
+ }
204
+ } else {
205
+ this.sendOperations()
206
+ }
207
+ }
208
+
177
209
  destroy() {
210
+ this.clearDebounceTimer()
178
211
  this.disconnect()
179
212
 
180
213
  this.doc.off(`update`, this.documentUpdateHandler)
@@ -187,6 +220,12 @@ export class ElectricProvider<
187
220
  }
188
221
 
189
222
  disconnect() {
223
+ // Flush any pending changes before disconnecting
224
+ this.clearDebounceTimer()
225
+ if (this.pendingChanges && this.connected) {
226
+ this.sendOperations()
227
+ }
228
+
190
229
  this.unsubscribeShapes?.()
191
230
 
192
231
  if (!this.connected) {
@@ -234,7 +273,7 @@ export class ElectricProvider<
234
273
  })
235
274
 
236
275
  const operationsShapeUnsubscribe = operationsStream.subscribe(
237
- (messages) => {
276
+ (messages: Message<RowWithDocumentUpdate>[]) => {
238
277
  this.operationsShapeHandler(
239
278
  messages,
240
279
  operationsStream.lastOffset,
@@ -247,17 +286,15 @@ export class ElectricProvider<
247
286
  if (this.awarenessUpdates) {
248
287
  const awarenessStream = new ShapeStream<RowWithAwarenessUpdate>({
249
288
  ...this.awarenessUpdates.shape,
250
- ...this.resumeState.awareness,
251
289
  signal: abortController.signal,
290
+ offset: `now`,
252
291
  })
253
292
 
254
- awarenessShapeUnsubscribe = awarenessStream.subscribe((messages) => {
255
- this.awarenessShapeHandler(
256
- messages,
257
- awarenessStream.lastOffset,
258
- awarenessStream.shapeHandle!
259
- )
260
- })
293
+ awarenessShapeUnsubscribe = awarenessStream.subscribe(
294
+ (messages: Message<RowWithAwarenessUpdate>[]) => {
295
+ this.awarenessShapeHandler(messages)
296
+ }
297
+ )
261
298
  }
262
299
 
263
300
  this.unsubscribeShapes = () => {
@@ -301,8 +338,6 @@ export class ElectricProvider<
301
338
  }
302
339
  }
303
340
 
304
- // TODO: add an optional throttler that batches updates
305
- // before pushing to the server
306
341
  private async applyDocumentUpdate(update: Uint8Array, origin: unknown) {
307
342
  // don't re-send updates from electric
308
343
  if (origin === `server`) {
@@ -310,10 +345,12 @@ export class ElectricProvider<
310
345
  }
311
346
 
312
347
  this.batch(update)
313
- this.sendOperations()
348
+ this.scheduleSendOperations()
314
349
  }
315
350
 
316
351
  private async sendOperations() {
352
+ this.clearDebounceTimer()
353
+
317
354
  if (!this.connected || this.sendingPendingChanges) {
318
355
  return
319
356
  }
@@ -397,11 +434,7 @@ export class ElectricProvider<
397
434
  }
398
435
  }
399
436
 
400
- private awarenessShapeHandler(
401
- messages: Message<RowWithAwarenessUpdate>[],
402
- offset: Offset,
403
- handle: string
404
- ) {
437
+ private awarenessShapeHandler(messages: Message<RowWithAwarenessUpdate>[]) {
405
438
  for (const message of messages) {
406
439
  if (isChangeMessage(message)) {
407
440
  if (message.headers.operation === `delete`) {
@@ -418,15 +451,6 @@ export class ElectricProvider<
418
451
  this
419
452
  )
420
453
  }
421
- } else if (
422
- isControlMessage(message) &&
423
- message.headers.control === `up-to-date`
424
- ) {
425
- this.resumeState.awareness = {
426
- offset: offset,
427
- handle: handle,
428
- }
429
- this.emit(`resumeState`, [this.resumeState])
430
454
  }
431
455
  }
432
456
  }