@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/README.md CHANGED
@@ -9,20 +9,26 @@ The typical flow for syncing shared documents using Yjs and Electric is the foll
9
9
  1. Developer exposes a shape proxy for [authorizing](https://electric-sql.com/docs/guides/auth) shape requests
10
10
  2. Clients define a [shape](https://electric-sql.com/docs/guides/shapes) for syncing changes for a [Y.Doc](https://docs.yjs.dev/api/y.doc)
11
11
  3. Developer exposes a [write API](#Handling Writes) for handling Yjs updates
12
- 4. Vòila! Y-Electric automatically shares updates across all connected clients
12
+ 4. Voilà! Y-Electric automatically shares updates across all connected clients
13
+
14
+ ### Key Features
13
15
 
14
16
  ### Basic Setup
15
17
 
16
18
  ```typescript
17
19
  import * as Y from 'yjs'
18
- import { ElectricProvider } from '@electric-sql/y-electric'
20
+ import {
21
+ ElectricProvider,
22
+ LocalStorageResumeStateProvider,
23
+ } from '@electric-sql/y-electric'
19
24
  import { Awareness } from 'y-protocols/awareness'
20
25
  import { parseToDecoder } from '@electric-sql/y-electric/utils'
21
26
 
22
27
  const ydoc = new Y.Doc()
23
28
  const awareness = new Awareness(ydoc)
29
+ const resumeStateProvider = new LocalStorageResumeStateProvider('my-doc')
24
30
 
25
- new ElectricProvider({
31
+ const provider = new ElectricProvider({
26
32
  doc: ydoc,
27
33
  documentUpdates: {
28
34
  shape: {
@@ -51,6 +57,9 @@ new ElectricProvider({
51
57
  },
52
58
  resumeState: resumeStateProvider.load(),
53
59
  })
60
+
61
+ // Subscribe to resume state changes to persist them
62
+ resumeStateProvider.subscribeToResumeState(provider)
54
63
  ```
55
64
 
56
65
  ### Handling Writes
@@ -116,6 +125,4 @@ In the client, you need to pass a `getUpdateFromRow` to extract the column with
116
125
 
117
126
  ### Storage providers
118
127
 
119
- Y-Electric work with existing [database providers](https://docs.yjs.dev/ecosystem/database-provider) to store documents locally. When saving documents locally, we recommend using the ElectricStorageProvider to save a resume point for the shapes, otherwise the entire document will be retransmitted when a new client session starts.
120
-
121
- The ElectricStorageProvider also keeps track of the document state vector to handle offline updates.
128
+ Y-Electric works with existing [database providers](https://docs.yjs.dev/ecosystem/database-provider) to store documents locally. When saving documents locally, we recommend providing a `ElectricResumeStateProvider` to save a resume point for the document, otherwise the entire document will be retransmitted when a new client session starts.
@@ -82,6 +82,7 @@ var ElectricProvider = class extends import_observable.ObservableV2 {
82
82
  * @param {ResumeState} [options.resumeState] - Resume state for the provider
83
83
  * @param {boolean} [options.connect=true] - Whether to automatically connect upon initialization
84
84
  * @param {typeof fetch} [options.fetchClient] - Custom fetch implementation to use for HTTP requests
85
+ * @param {number} [options.debounceMs] - Debounce window in milliseconds for sending document updates. If 0 or undefined, debouncing is disabled.
85
86
  */
86
87
  constructor({
87
88
  doc,
@@ -89,7 +90,8 @@ var ElectricProvider = class extends import_observable.ObservableV2 {
89
90
  awarenessUpdates: awarenessUpdatesConfig,
90
91
  resumeState,
91
92
  connect = true,
92
- fetchClient
93
+ fetchClient,
94
+ debounceMs
93
95
  }) {
94
96
  var _a;
95
97
  super();
@@ -99,10 +101,12 @@ var ElectricProvider = class extends import_observable.ObservableV2 {
99
101
  this.pendingChanges = null;
100
102
  this.sendingAwarenessState = false;
101
103
  this.pendingAwarenessUpdate = null;
104
+ this.debounceTimer = null;
102
105
  this.doc = doc;
103
106
  this.documentUpdates = documentUpdatesConfig;
104
107
  this.awarenessUpdates = awarenessUpdatesConfig;
105
108
  this.resumeState = resumeState != null ? resumeState : {};
109
+ this.debounceMs = debounceMs != null ? debounceMs : 0;
106
110
  this.fetchClient = fetchClient;
107
111
  this.exitHandler = () => {
108
112
  if (env.isNode && typeof process !== `undefined`) {
@@ -156,8 +160,30 @@ var ElectricProvider = class extends import_observable.ObservableV2 {
156
160
  this.pendingChanges = update;
157
161
  }
158
162
  }
163
+ clearDebounceTimer() {
164
+ if (this.debounceTimer !== null) {
165
+ clearTimeout(this.debounceTimer);
166
+ this.debounceTimer = null;
167
+ }
168
+ }
169
+ scheduleSendOperations() {
170
+ if (this.debounceMs > 0) {
171
+ if (this.debounceTimer === null) {
172
+ this.debounceTimer = setTimeout(async () => {
173
+ this.debounceTimer = null;
174
+ await this.sendOperations();
175
+ if (this.pendingChanges && this.connected && !this.sendingPendingChanges) {
176
+ this.scheduleSendOperations();
177
+ }
178
+ }, this.debounceMs);
179
+ }
180
+ } else {
181
+ this.sendOperations();
182
+ }
183
+ }
159
184
  destroy() {
160
185
  var _a;
186
+ this.clearDebounceTimer();
161
187
  this.disconnect();
162
188
  this.doc.off(`update`, this.documentUpdateHandler);
163
189
  (_a = this.awarenessUpdates) == null ? void 0 : _a.protocol.off(`update`, this.awarenessUpdateHandler);
@@ -168,6 +194,10 @@ var ElectricProvider = class extends import_observable.ObservableV2 {
168
194
  }
169
195
  disconnect() {
170
196
  var _a;
197
+ this.clearDebounceTimer();
198
+ if (this.pendingChanges && this.connected) {
199
+ this.sendOperations();
200
+ }
171
201
  (_a = this.unsubscribeShapes) == null ? void 0 : _a.call(this);
172
202
  if (!this.connected) {
173
203
  return;
@@ -211,16 +241,15 @@ var ElectricProvider = class extends import_observable.ObservableV2 {
211
241
  );
212
242
  let awarenessShapeUnsubscribe;
213
243
  if (this.awarenessUpdates) {
214
- const awarenessStream = new import_client.ShapeStream(__spreadProps(__spreadValues(__spreadValues({}, this.awarenessUpdates.shape), this.resumeState.awareness), {
215
- signal: abortController.signal
244
+ const awarenessStream = new import_client.ShapeStream(__spreadProps(__spreadValues({}, this.awarenessUpdates.shape), {
245
+ signal: abortController.signal,
246
+ offset: `now`
216
247
  }));
217
- awarenessShapeUnsubscribe = awarenessStream.subscribe((messages) => {
218
- this.awarenessShapeHandler(
219
- messages,
220
- awarenessStream.lastOffset,
221
- awarenessStream.shapeHandle
222
- );
223
- });
248
+ awarenessShapeUnsubscribe = awarenessStream.subscribe(
249
+ (messages) => {
250
+ this.awarenessShapeHandler(messages);
251
+ }
252
+ );
224
253
  }
225
254
  this.unsubscribeShapes = () => {
226
255
  abortController.abort();
@@ -252,17 +281,16 @@ var ElectricProvider = class extends import_observable.ObservableV2 {
252
281
  }
253
282
  }
254
283
  }
255
- // TODO: add an optional throttler that batches updates
256
- // before pushing to the server
257
284
  async applyDocumentUpdate(update, origin) {
258
285
  if (origin === `server`) {
259
286
  return;
260
287
  }
261
288
  this.batch(update);
262
- this.sendOperations();
289
+ this.scheduleSendOperations();
263
290
  }
264
291
  async sendOperations() {
265
292
  var _a;
293
+ this.clearDebounceTimer();
266
294
  if (!this.connected || this.sendingPendingChanges) {
267
295
  return;
268
296
  }
@@ -328,7 +356,7 @@ var ElectricProvider = class extends import_observable.ObservableV2 {
328
356
  this.sendingAwarenessState = false;
329
357
  }
330
358
  }
331
- awarenessShapeHandler(messages, offset, handle) {
359
+ awarenessShapeHandler(messages) {
332
360
  for (const message of messages) {
333
361
  if ((0, import_client.isChangeMessage)(message)) {
334
362
  if (message.headers.operation === `delete`) {
@@ -345,12 +373,6 @@ var ElectricProvider = class extends import_observable.ObservableV2 {
345
373
  this
346
374
  );
347
375
  }
348
- } else if ((0, import_client.isControlMessage)(message) && message.headers.control === `up-to-date`) {
349
- this.resumeState.awareness = {
350
- offset,
351
- handle
352
- };
353
- this.emit(`resumeState`, [this.resumeState]);
354
376
  }
355
377
  }
356
378
  }
@@ -394,8 +416,7 @@ var LocalStorageResumeStateProvider = class extends import_observable2.Observabl
394
416
  }
395
417
  save(resumeState) {
396
418
  const jsonPart = JSON.stringify({
397
- operations: resumeState.document,
398
- awareness: resumeState.awareness
419
+ operations: resumeState.document
399
420
  });
400
421
  localStorage.setItem(this.key, jsonPart);
401
422
  if (resumeState.stableStateVector) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/index.ts","../../src/y-electric.ts","../../src/local-storage-resume-state.ts","../../src/utils.ts"],"sourcesContent":["export * from './y-electric'\nexport * from './local-storage-resume-state'\nexport * from './utils'\nexport * from './types'\n","import * as encoding from 'lib0/encoding'\nimport * as decoding from 'lib0/decoding'\nimport * as awarenessProtocol from 'y-protocols/awareness'\nimport { ObservableV2 } from 'lib0/observable'\nimport * as env from 'lib0/environment'\nimport * as Y from 'yjs'\nimport {\n GetExtensions,\n isChangeMessage,\n isControlMessage,\n Message,\n Offset,\n Row,\n ShapeStream,\n ShapeStreamOptions,\n} from '@electric-sql/client'\nimport {\n YProvider,\n ResumeState,\n SendErrorRetryHandler,\n ElectricProviderOptions,\n} from './types'\n\ntype AwarenessUpdate = {\n added: number[]\n updated: number[]\n removed: number[]\n}\n\nexport class ElectricProvider<\n RowWithDocumentUpdate extends Row<decoding.Decoder> = never,\n RowWithAwarenessUpdate extends Row<decoding.Decoder> = never,\n> extends ObservableV2<YProvider> {\n private doc: Y.Doc\n\n private documentUpdates: {\n shape: ShapeStreamOptions<GetExtensions<RowWithDocumentUpdate>>\n sendUrl: string | URL\n getUpdateFromRow: (row: RowWithDocumentUpdate) => decoding.Decoder\n sendErrorRetryHandler?: SendErrorRetryHandler\n }\n\n private awarenessUpdates?: {\n shape: ShapeStreamOptions<GetExtensions<RowWithAwarenessUpdate>>\n sendUrl: string | URL\n protocol: awarenessProtocol.Awareness\n getUpdateFromRow: (row: RowWithAwarenessUpdate) => decoding.Decoder\n sendErrorRetryHandler?: SendErrorRetryHandler\n }\n\n private _connected: boolean = false\n private _synced: boolean = false\n\n private resumeState: ResumeState\n private sendingPendingChanges: boolean = false\n private pendingChanges: Uint8Array | null = null\n private sendingAwarenessState: boolean = false\n private pendingAwarenessUpdate: AwarenessUpdate | null = null\n\n private documentUpdateHandler: (\n update: Uint8Array,\n origin: unknown,\n doc: Y.Doc,\n transaction: Y.Transaction\n ) => void\n private awarenessUpdateHandler?: (\n update: AwarenessUpdate,\n origin: unknown\n ) => void\n\n private exitHandler: () => void\n private unsubscribeShapes?: () => void\n\n private fetchClient?: typeof fetch\n\n /**\n * Creates a new ElectricProvider instance that connects YJS documents to Electric SQL.\n *\n * @constructor\n * @param {ElectricProviderOptions} options - Configuration options for the provider\n * @param {Y.Doc} options.doc - The YJS document to be synchronized\n * @param {Object} options.documentUpdates - Document updates configuration\n * @param {ShapeStreamOptions} options.documentUpdates.shape - Options for the document updates shape stream\n * @param {string|URL} options.documentUpdates.sendUrl - URL endpoint for sending document updates\n * @param {Function} options.documentUpdates.getUpdateFromRow - Function to extract document update from row\n * @param {SendErrorRetryHandler} [options.documentUpdates.sendErrorRetryHandler] - Error handler for retrying document updates\n * @param {Object} [options.awarenessUpdates] - Awareness updates configuration (optional)\n * @param {ShapeStreamOptions} options.awarenessUpdates.shape - Options for the awareness updates shape stream\n * @param {string|URL} options.awarenessUpdates.sendUrl - URL endpoint for sending awareness updates\n * @param {awarenessProtocol.Awareness} options.awarenessUpdates.protocol - Awareness protocol instance\n * @param {Function} options.awarenessUpdates.getUpdateFromRow - Function to extract awareness update from row\n * @param {SendErrorRetryHandler} [options.awarenessUpdates.sendErrorRetryHandler] - Error handler for retrying awareness updates\n * @param {ResumeState} [options.resumeState] - Resume state for the provider\n * @param {boolean} [options.connect=true] - Whether to automatically connect upon initialization\n * @param {typeof fetch} [options.fetchClient] - Custom fetch implementation to use for HTTP requests\n */\n constructor({\n doc,\n documentUpdates: documentUpdatesConfig,\n awarenessUpdates: awarenessUpdatesConfig,\n resumeState,\n connect = true,\n fetchClient,\n }: ElectricProviderOptions<RowWithDocumentUpdate, RowWithAwarenessUpdate>) {\n super()\n\n this.doc = doc\n this.documentUpdates = documentUpdatesConfig\n this.awarenessUpdates = awarenessUpdatesConfig\n this.resumeState = resumeState ?? {}\n\n this.fetchClient = fetchClient\n\n this.exitHandler = () => {\n if (env.isNode && typeof process !== `undefined`) {\n process.on(`exit`, this.destroy.bind(this))\n }\n }\n\n this.documentUpdateHandler = this.doc.on(\n `update`,\n this.applyDocumentUpdate.bind(this)\n )\n if (this.awarenessUpdates) {\n this.awarenessUpdateHandler = this.applyAwarenessUpdate.bind(this)\n this.awarenessUpdates.protocol.on(`update`, this.awarenessUpdateHandler!)\n }\n\n // enqueue unsynced changes from document if the\n // resume state provides the document state vector\n if (this.resumeState?.stableStateVector) {\n this.pendingChanges = Y.encodeStateAsUpdate(\n this.doc,\n this.resumeState.stableStateVector\n )\n }\n\n if (connect) {\n this.connect()\n }\n }\n\n get synced() {\n return this._synced\n }\n\n set synced(state) {\n if (this._synced !== state) {\n this._synced = state\n this.emit(`synced`, [state])\n this.emit(`sync`, [state])\n }\n }\n\n set connected(state) {\n if (this._connected !== state) {\n this._connected = state\n if (state) {\n this.sendOperations()\n }\n this.emit(`status`, [{ status: state ? `connected` : `disconnected` }])\n }\n }\n\n get connected() {\n return this._connected\n }\n\n private batch(update: Uint8Array) {\n if (this.pendingChanges) {\n this.pendingChanges = Y.mergeUpdates([this.pendingChanges, update])\n } else {\n this.pendingChanges = update\n }\n }\n\n destroy() {\n this.disconnect()\n\n this.doc.off(`update`, this.documentUpdateHandler)\n this.awarenessUpdates?.protocol.off(`update`, this.awarenessUpdateHandler!)\n\n if (env.isNode && typeof process !== `undefined`) {\n process.off(`exit`, this.exitHandler!)\n }\n super.destroy()\n }\n\n disconnect() {\n this.unsubscribeShapes?.()\n\n if (!this.connected) {\n return\n }\n\n if (this.awarenessUpdates) {\n awarenessProtocol.removeAwarenessStates(\n this.awarenessUpdates.protocol,\n Array.from(this.awarenessUpdates.protocol.getStates().keys()).filter(\n (client) => client !== this.awarenessUpdates!.protocol.clientID\n ),\n this\n )\n\n // try to notifying other clients that we are disconnecting\n awarenessProtocol.removeAwarenessStates(\n this.awarenessUpdates.protocol,\n [this.awarenessUpdates.protocol.clientID],\n `local`\n )\n\n this.awarenessUpdates.protocol.setLocalState({})\n }\n\n // TODO: await for events before closing\n this.emit(`connection-close`, [])\n\n this.pendingAwarenessUpdate = null\n\n this.connected = false\n this.synced = false\n }\n\n connect() {\n if (this.connected) {\n return\n }\n const abortController = new AbortController()\n\n const operationsStream = new ShapeStream<RowWithDocumentUpdate>({\n ...this.documentUpdates.shape,\n ...this.resumeState.document,\n signal: abortController.signal,\n })\n\n const operationsShapeUnsubscribe = operationsStream.subscribe(\n (messages) => {\n this.operationsShapeHandler(\n messages,\n operationsStream.lastOffset,\n operationsStream.shapeHandle!\n )\n }\n )\n\n let awarenessShapeUnsubscribe: () => void | undefined\n if (this.awarenessUpdates) {\n const awarenessStream = new ShapeStream<RowWithAwarenessUpdate>({\n ...this.awarenessUpdates.shape,\n ...this.resumeState.awareness,\n signal: abortController.signal,\n })\n\n awarenessShapeUnsubscribe = awarenessStream.subscribe((messages) => {\n this.awarenessShapeHandler(\n messages,\n awarenessStream.lastOffset,\n awarenessStream.shapeHandle!\n )\n })\n }\n\n this.unsubscribeShapes = () => {\n abortController.abort()\n operationsShapeUnsubscribe()\n awarenessShapeUnsubscribe?.()\n this.unsubscribeShapes = undefined\n }\n\n this.emit(`status`, [{ status: `connecting` }])\n }\n\n private operationsShapeHandler(\n messages: Message<RowWithDocumentUpdate>[],\n offset: Offset,\n handle: string\n ) {\n for (const message of messages) {\n if (isChangeMessage(message)) {\n const decoder = this.documentUpdates.getUpdateFromRow(message.value)\n while (decoder.pos !== decoder.arr.length) {\n const operation = decoding.readVarUint8Array(decoder)\n Y.applyUpdate(this.doc, operation, `server`)\n }\n } else if (\n isControlMessage(message) &&\n message.headers.control === `up-to-date`\n ) {\n this.resumeState.document = {\n offset,\n handle,\n }\n\n if (!this.sendingPendingChanges) {\n this.synced = true\n this.resumeState.stableStateVector = Y.encodeStateVector(this.doc)\n }\n this.emit(`resumeState`, [this.resumeState])\n this.connected = true\n }\n }\n }\n\n // TODO: add an optional throttler that batches updates\n // before pushing to the server\n private async applyDocumentUpdate(update: Uint8Array, origin: unknown) {\n // don't re-send updates from electric\n if (origin === `server`) {\n return\n }\n\n this.batch(update)\n this.sendOperations()\n }\n\n private async sendOperations() {\n if (!this.connected || this.sendingPendingChanges) {\n return\n }\n\n try {\n this.sendingPendingChanges = true\n while (\n this.pendingChanges &&\n this.pendingChanges.length > 2 &&\n this.connected\n ) {\n const sending = this.pendingChanges\n this.pendingChanges = null\n\n const encoder = encoding.createEncoder()\n encoding.writeVarUint8Array(encoder, sending)\n\n const success = await send(\n encoder,\n this.documentUpdates.sendUrl,\n this.fetchClient ?? fetch,\n this.documentUpdates.sendErrorRetryHandler\n )\n if (!success) {\n this.batch(sending)\n this.disconnect()\n }\n }\n // no more pending changes, move stableStateVector forward\n this.resumeState.stableStateVector = Y.encodeStateVector(this.doc)\n this.emit(`resumeState`, [this.resumeState])\n } finally {\n this.sendingPendingChanges = false\n }\n }\n\n private async applyAwarenessUpdate(\n awarenessUpdate: AwarenessUpdate,\n origin: unknown\n ) {\n if (origin !== `local` || !this.connected) {\n return\n }\n\n this.pendingAwarenessUpdate = awarenessUpdate\n\n if (this.sendingAwarenessState) {\n return\n }\n\n this.sendingAwarenessState = true\n\n try {\n while (this.pendingAwarenessUpdate && this.connected) {\n const update = this.pendingAwarenessUpdate\n this.pendingAwarenessUpdate = null\n\n const { added, updated, removed } = update\n const changedClients = added.concat(updated).concat(removed)\n const encoder = encoding.createEncoder()\n\n encoding.writeVarUint8Array(\n encoder,\n awarenessProtocol.encodeAwarenessUpdate(\n this.awarenessUpdates!.protocol,\n changedClients\n )\n )\n const success = await send(\n encoder,\n this.awarenessUpdates!.sendUrl,\n this.fetchClient ?? fetch,\n this.awarenessUpdates!.sendErrorRetryHandler\n )\n if (!success) {\n this.disconnect()\n }\n }\n } finally {\n this.sendingAwarenessState = false\n }\n }\n\n private awarenessShapeHandler(\n messages: Message<RowWithAwarenessUpdate>[],\n offset: Offset,\n handle: string\n ) {\n for (const message of messages) {\n if (isChangeMessage(message)) {\n if (message.headers.operation === `delete`) {\n awarenessProtocol.removeAwarenessStates(\n this.awarenessUpdates!.protocol,\n [Number(message.value.client_id)],\n `remote`\n )\n } else {\n const decoder = this.awarenessUpdates!.getUpdateFromRow(message.value)\n awarenessProtocol.applyAwarenessUpdate(\n this.awarenessUpdates!.protocol,\n decoding.readVarUint8Array(decoder),\n this\n )\n }\n } else if (\n isControlMessage(message) &&\n message.headers.control === `up-to-date`\n ) {\n this.resumeState.awareness = {\n offset: offset,\n handle: handle,\n }\n this.emit(`resumeState`, [this.resumeState])\n }\n }\n }\n}\n\nasync function send(\n encoder: encoding.Encoder,\n endpoint: string | URL,\n fetchClient: typeof fetch,\n retryHandler?: SendErrorRetryHandler\n): Promise<boolean> {\n let response: Response | undefined\n const op = encoding.toUint8Array(encoder)\n\n try {\n response = await fetchClient(endpoint!, {\n method: `PUT`,\n headers: {\n 'Content-Type': `application/octet-stream`,\n },\n body: op as BodyInit,\n })\n\n if (!response.ok) {\n throw new Error(`Server did not return 2xx`)\n }\n\n return true\n } catch (error) {\n const shouldRetry = await (retryHandler?.({\n response,\n error,\n }) ?? false)\n return shouldRetry\n }\n}\n","import { ResumeState, ElectricResumeStateProvider } from './types'\nimport { ObservableV2 } from 'lib0/observable.js'\nimport { ElectricProvider } from './y-electric'\nimport * as buffer from 'lib0/buffer'\n\n/**\n * A ResumeStateProvider implementation using LocalStorage.\n * This is a reference implementation that can be used as a starting point\n * for implementing other ResumeStateProviders.\n */\nexport class LocalStorageResumeStateProvider extends ObservableV2<ElectricResumeStateProvider> {\n private key: string\n private resumeState?: ResumeState\n\n constructor(key: string) {\n super()\n this.key = key\n }\n\n subscribeToResumeState(provider: ElectricProvider): () => void {\n const resumeStateHandler = provider.on(`resumeState`, this.save.bind(this))\n return () => provider.off(`resumeState`, resumeStateHandler)\n }\n\n save(resumeState: ResumeState) {\n const jsonPart = JSON.stringify({\n operations: resumeState.document,\n awareness: resumeState.awareness,\n })\n localStorage.setItem(this.key, jsonPart)\n\n if (resumeState.stableStateVector) {\n const vectorBase64 = buffer.toBase64(resumeState.stableStateVector)\n localStorage.setItem(`${this.key}_vector`, vectorBase64)\n } else {\n // ensure vector is removed\n localStorage.removeItem(`${this.key}_vector`)\n }\n }\n\n load(): ResumeState {\n if (this.resumeState) {\n return this.resumeState\n }\n\n const jsonData = localStorage.getItem(this.key)\n if (!jsonData) {\n this.emit(`synced`, [{}])\n } else {\n this.resumeState = JSON.parse(jsonData)\n\n const vectorData = localStorage.getItem(`${this.key}_vector`)\n if (vectorData) {\n this.resumeState!.stableStateVector = buffer.fromBase64(vectorData)\n }\n\n this.emit(`synced`, [this.resumeState!])\n }\n\n return this.resumeState!\n }\n}\n","import * as decoding from 'lib0/decoding'\n\n/**\n * Convert a hex string from PostgreSQL's bytea format to a Uint8Array\n */\nconst hexStringToUint8Array = (hexString: string) => {\n const cleanHexString = hexString.startsWith(`\\\\x`)\n ? hexString.slice(2)\n : hexString\n return new Uint8Array(\n cleanHexString.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16))\n )\n}\n\n/**\n * Utility to parse hex string bytea data to a decoder for YJS operations\n */\nexport const parseToDecoder = {\n bytea: (hexString: string) => {\n const uint8Array = hexStringToUint8Array(hexString)\n return decoding.createDecoder(uint8Array)\n },\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,eAA0B;AAC1B,eAA0B;AAC1B,wBAAmC;AACnC,wBAA6B;AAC7B,UAAqB;AACrB,QAAmB;AACnB,oBASO;AAcA,IAAM,mBAAN,cAGG,+BAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgEhC,YAAY;AAAA,IACV;AAAA,IACA,iBAAiB;AAAA,IACjB,kBAAkB;AAAA,IAClB;AAAA,IACA,UAAU;AAAA,IACV;AAAA,EACF,GAA2E;AAvG7E;AAwGI,UAAM;AAtDR,SAAQ,aAAsB;AAC9B,SAAQ,UAAmB;AAG3B,SAAQ,wBAAiC;AACzC,SAAQ,iBAAoC;AAC5C,SAAQ,wBAAiC;AACzC,SAAQ,yBAAiD;AAiDvD,SAAK,MAAM;AACX,SAAK,kBAAkB;AACvB,SAAK,mBAAmB;AACxB,SAAK,cAAc,oCAAe,CAAC;AAEnC,SAAK,cAAc;AAEnB,SAAK,cAAc,MAAM;AACvB,UAAQ,cAAU,OAAO,YAAY,aAAa;AAChD,gBAAQ,GAAG,QAAQ,KAAK,QAAQ,KAAK,IAAI,CAAC;AAAA,MAC5C;AAAA,IACF;AAEA,SAAK,wBAAwB,KAAK,IAAI;AAAA,MACpC;AAAA,MACA,KAAK,oBAAoB,KAAK,IAAI;AAAA,IACpC;AACA,QAAI,KAAK,kBAAkB;AACzB,WAAK,yBAAyB,KAAK,qBAAqB,KAAK,IAAI;AACjE,WAAK,iBAAiB,SAAS,GAAG,UAAU,KAAK,sBAAuB;AAAA,IAC1E;AAIA,SAAI,UAAK,gBAAL,mBAAkB,mBAAmB;AACvC,WAAK,iBAAmB;AAAA,QACtB,KAAK;AAAA,QACL,KAAK,YAAY;AAAA,MACnB;AAAA,IACF;AAEA,QAAI,SAAS;AACX,WAAK,QAAQ;AAAA,IACf;AAAA,EACF;AAAA,EAEA,IAAI,SAAS;AACX,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,OAAO,OAAO;AAChB,QAAI,KAAK,YAAY,OAAO;AAC1B,WAAK,UAAU;AACf,WAAK,KAAK,UAAU,CAAC,KAAK,CAAC;AAC3B,WAAK,KAAK,QAAQ,CAAC,KAAK,CAAC;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,IAAI,UAAU,OAAO;AACnB,QAAI,KAAK,eAAe,OAAO;AAC7B,WAAK,aAAa;AAClB,UAAI,OAAO;AACT,aAAK,eAAe;AAAA,MACtB;AACA,WAAK,KAAK,UAAU,CAAC,EAAE,QAAQ,QAAQ,cAAc,eAAe,CAAC,CAAC;AAAA,IACxE;AAAA,EACF;AAAA,EAEA,IAAI,YAAY;AACd,WAAO,KAAK;AAAA,EACd;AAAA,EAEQ,MAAM,QAAoB;AAChC,QAAI,KAAK,gBAAgB;AACvB,WAAK,iBAAmB,eAAa,CAAC,KAAK,gBAAgB,MAAM,CAAC;AAAA,IACpE,OAAO;AACL,WAAK,iBAAiB;AAAA,IACxB;AAAA,EACF;AAAA,EAEA,UAAU;AAhLZ;AAiLI,SAAK,WAAW;AAEhB,SAAK,IAAI,IAAI,UAAU,KAAK,qBAAqB;AACjD,eAAK,qBAAL,mBAAuB,SAAS,IAAI,UAAU,KAAK;AAEnD,QAAQ,cAAU,OAAO,YAAY,aAAa;AAChD,cAAQ,IAAI,QAAQ,KAAK,WAAY;AAAA,IACvC;AACA,UAAM,QAAQ;AAAA,EAChB;AAAA,EAEA,aAAa;AA5Lf;AA6LI,eAAK,sBAAL;AAEA,QAAI,CAAC,KAAK,WAAW;AACnB;AAAA,IACF;AAEA,QAAI,KAAK,kBAAkB;AACzB,MAAkB;AAAA,QAChB,KAAK,iBAAiB;AAAA,QACtB,MAAM,KAAK,KAAK,iBAAiB,SAAS,UAAU,EAAE,KAAK,CAAC,EAAE;AAAA,UAC5D,CAAC,WAAW,WAAW,KAAK,iBAAkB,SAAS;AAAA,QACzD;AAAA,QACA;AAAA,MACF;AAGA,MAAkB;AAAA,QAChB,KAAK,iBAAiB;AAAA,QACtB,CAAC,KAAK,iBAAiB,SAAS,QAAQ;AAAA,QACxC;AAAA,MACF;AAEA,WAAK,iBAAiB,SAAS,cAAc,CAAC,CAAC;AAAA,IACjD;AAGA,SAAK,KAAK,oBAAoB,CAAC,CAAC;AAEhC,SAAK,yBAAyB;AAE9B,SAAK,YAAY;AACjB,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,UAAU;AACR,QAAI,KAAK,WAAW;AAClB;AAAA,IACF;AACA,UAAM,kBAAkB,IAAI,gBAAgB;AAE5C,UAAM,mBAAmB,IAAI,0BAAmC,gDAC3D,KAAK,gBAAgB,QACrB,KAAK,YAAY,WAF0C;AAAA,MAG9D,QAAQ,gBAAgB;AAAA,IAC1B,EAAC;AAED,UAAM,6BAA6B,iBAAiB;AAAA,MAClD,CAAC,aAAa;AACZ,aAAK;AAAA,UACH;AAAA,UACA,iBAAiB;AAAA,UACjB,iBAAiB;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAEA,QAAI;AACJ,QAAI,KAAK,kBAAkB;AACzB,YAAM,kBAAkB,IAAI,0BAAoC,gDAC3D,KAAK,iBAAiB,QACtB,KAAK,YAAY,YAF0C;AAAA,QAG9D,QAAQ,gBAAgB;AAAA,MAC1B,EAAC;AAED,kCAA4B,gBAAgB,UAAU,CAAC,aAAa;AAClE,aAAK;AAAA,UACH;AAAA,UACA,gBAAgB;AAAA,UAChB,gBAAgB;AAAA,QAClB;AAAA,MACF,CAAC;AAAA,IACH;AAEA,SAAK,oBAAoB,MAAM;AAC7B,sBAAgB,MAAM;AACtB,iCAA2B;AAC3B;AACA,WAAK,oBAAoB;AAAA,IAC3B;AAEA,SAAK,KAAK,UAAU,CAAC,EAAE,QAAQ,aAAa,CAAC,CAAC;AAAA,EAChD;AAAA,EAEQ,uBACN,UACA,QACA,QACA;AACA,eAAW,WAAW,UAAU;AAC9B,cAAI,+BAAgB,OAAO,GAAG;AAC5B,cAAM,UAAU,KAAK,gBAAgB,iBAAiB,QAAQ,KAAK;AACnE,eAAO,QAAQ,QAAQ,QAAQ,IAAI,QAAQ;AACzC,gBAAM,YAAqB,2BAAkB,OAAO;AACpD,UAAE,cAAY,KAAK,KAAK,WAAW,QAAQ;AAAA,QAC7C;AAAA,MACF,eACE,gCAAiB,OAAO,KACxB,QAAQ,QAAQ,YAAY,cAC5B;AACA,aAAK,YAAY,WAAW;AAAA,UAC1B;AAAA,UACA;AAAA,QACF;AAEA,YAAI,CAAC,KAAK,uBAAuB;AAC/B,eAAK,SAAS;AACd,eAAK,YAAY,oBAAsB,oBAAkB,KAAK,GAAG;AAAA,QACnE;AACA,aAAK,KAAK,eAAe,CAAC,KAAK,WAAW,CAAC;AAC3C,aAAK,YAAY;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA,EAIA,MAAc,oBAAoB,QAAoB,QAAiB;AAErE,QAAI,WAAW,UAAU;AACvB;AAAA,IACF;AAEA,SAAK,MAAM,MAAM;AACjB,SAAK,eAAe;AAAA,EACtB;AAAA,EAEA,MAAc,iBAAiB;AA3TjC;AA4TI,QAAI,CAAC,KAAK,aAAa,KAAK,uBAAuB;AACjD;AAAA,IACF;AAEA,QAAI;AACF,WAAK,wBAAwB;AAC7B,aACE,KAAK,kBACL,KAAK,eAAe,SAAS,KAC7B,KAAK,WACL;AACA,cAAM,UAAU,KAAK;AACrB,aAAK,iBAAiB;AAEtB,cAAM,UAAmB,uBAAc;AACvC,QAAS,4BAAmB,SAAS,OAAO;AAE5C,cAAM,UAAU,MAAM;AAAA,UACpB;AAAA,UACA,KAAK,gBAAgB;AAAA,WACrB,UAAK,gBAAL,YAAoB;AAAA,UACpB,KAAK,gBAAgB;AAAA,QACvB;AACA,YAAI,CAAC,SAAS;AACZ,eAAK,MAAM,OAAO;AAClB,eAAK,WAAW;AAAA,QAClB;AAAA,MACF;AAEA,WAAK,YAAY,oBAAsB,oBAAkB,KAAK,GAAG;AACjE,WAAK,KAAK,eAAe,CAAC,KAAK,WAAW,CAAC;AAAA,IAC7C,UAAE;AACA,WAAK,wBAAwB;AAAA,IAC/B;AAAA,EACF;AAAA,EAEA,MAAc,qBACZ,iBACA,QACA;AAnWJ;AAoWI,QAAI,WAAW,WAAW,CAAC,KAAK,WAAW;AACzC;AAAA,IACF;AAEA,SAAK,yBAAyB;AAE9B,QAAI,KAAK,uBAAuB;AAC9B;AAAA,IACF;AAEA,SAAK,wBAAwB;AAE7B,QAAI;AACF,aAAO,KAAK,0BAA0B,KAAK,WAAW;AACpD,cAAM,SAAS,KAAK;AACpB,aAAK,yBAAyB;AAE9B,cAAM,EAAE,OAAO,SAAS,QAAQ,IAAI;AACpC,cAAM,iBAAiB,MAAM,OAAO,OAAO,EAAE,OAAO,OAAO;AAC3D,cAAM,UAAmB,uBAAc;AAEvC,QAAS;AAAA,UACP;AAAA,UACkB;AAAA,YAChB,KAAK,iBAAkB;AAAA,YACvB;AAAA,UACF;AAAA,QACF;AACA,cAAM,UAAU,MAAM;AAAA,UACpB;AAAA,UACA,KAAK,iBAAkB;AAAA,WACvB,UAAK,gBAAL,YAAoB;AAAA,UACpB,KAAK,iBAAkB;AAAA,QACzB;AACA,YAAI,CAAC,SAAS;AACZ,eAAK,WAAW;AAAA,QAClB;AAAA,MACF;AAAA,IACF,UAAE;AACA,WAAK,wBAAwB;AAAA,IAC/B;AAAA,EACF;AAAA,EAEQ,sBACN,UACA,QACA,QACA;AACA,eAAW,WAAW,UAAU;AAC9B,cAAI,+BAAgB,OAAO,GAAG;AAC5B,YAAI,QAAQ,QAAQ,cAAc,UAAU;AAC1C,UAAkB;AAAA,YAChB,KAAK,iBAAkB;AAAA,YACvB,CAAC,OAAO,QAAQ,MAAM,SAAS,CAAC;AAAA,YAChC;AAAA,UACF;AAAA,QACF,OAAO;AACL,gBAAM,UAAU,KAAK,iBAAkB,iBAAiB,QAAQ,KAAK;AACrE,UAAkB;AAAA,YAChB,KAAK,iBAAkB;AAAA,YACd,2BAAkB,OAAO;AAAA,YAClC;AAAA,UACF;AAAA,QACF;AAAA,MACF,eACE,gCAAiB,OAAO,KACxB,QAAQ,QAAQ,YAAY,cAC5B;AACA,aAAK,YAAY,YAAY;AAAA,UAC3B;AAAA,UACA;AAAA,QACF;AACA,aAAK,KAAK,eAAe,CAAC,KAAK,WAAW,CAAC;AAAA,MAC7C;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAe,KACb,SACA,UACA,aACA,cACkB;AAvbpB;AAwbE,MAAI;AACJ,QAAM,KAAc,sBAAa,OAAO;AAExC,MAAI;AACF,eAAW,MAAM,YAAY,UAAW;AAAA,MACtC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM;AAAA,IACR,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,UAAM,cAAc,QAAO,kDAAe;AAAA,MACxC;AAAA,MACA;AAAA,IACF,OAH2B,YAGrB;AACN,WAAO;AAAA,EACT;AACF;;;AC/cA,IAAAA,qBAA6B;AAE7B,aAAwB;AAOjB,IAAM,kCAAN,cAA8C,gCAA0C;AAAA,EAI7F,YAAY,KAAa;AACvB,UAAM;AACN,SAAK,MAAM;AAAA,EACb;AAAA,EAEA,uBAAuB,UAAwC;AAC7D,UAAM,qBAAqB,SAAS,GAAG,eAAe,KAAK,KAAK,KAAK,IAAI,CAAC;AAC1E,WAAO,MAAM,SAAS,IAAI,eAAe,kBAAkB;AAAA,EAC7D;AAAA,EAEA,KAAK,aAA0B;AAC7B,UAAM,WAAW,KAAK,UAAU;AAAA,MAC9B,YAAY,YAAY;AAAA,MACxB,WAAW,YAAY;AAAA,IACzB,CAAC;AACD,iBAAa,QAAQ,KAAK,KAAK,QAAQ;AAEvC,QAAI,YAAY,mBAAmB;AACjC,YAAM,eAAsB,gBAAS,YAAY,iBAAiB;AAClE,mBAAa,QAAQ,GAAG,KAAK,GAAG,WAAW,YAAY;AAAA,IACzD,OAAO;AAEL,mBAAa,WAAW,GAAG,KAAK,GAAG,SAAS;AAAA,IAC9C;AAAA,EACF;AAAA,EAEA,OAAoB;AAClB,QAAI,KAAK,aAAa;AACpB,aAAO,KAAK;AAAA,IACd;AAEA,UAAM,WAAW,aAAa,QAAQ,KAAK,GAAG;AAC9C,QAAI,CAAC,UAAU;AACb,WAAK,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC;AAAA,IAC1B,OAAO;AACL,WAAK,cAAc,KAAK,MAAM,QAAQ;AAEtC,YAAM,aAAa,aAAa,QAAQ,GAAG,KAAK,GAAG,SAAS;AAC5D,UAAI,YAAY;AACd,aAAK,YAAa,oBAA2B,kBAAW,UAAU;AAAA,MACpE;AAEA,WAAK,KAAK,UAAU,CAAC,KAAK,WAAY,CAAC;AAAA,IACzC;AAEA,WAAO,KAAK;AAAA,EACd;AACF;;;AC7DA,IAAAC,YAA0B;AAK1B,IAAM,wBAAwB,CAAC,cAAsB;AACnD,QAAM,iBAAiB,UAAU,WAAW,KAAK,IAC7C,UAAU,MAAM,CAAC,IACjB;AACJ,SAAO,IAAI;AAAA,IACT,eAAe,MAAM,SAAS,EAAG,IAAI,CAAC,SAAS,SAAS,MAAM,EAAE,CAAC;AAAA,EACnE;AACF;AAKO,IAAM,iBAAiB;AAAA,EAC5B,OAAO,CAAC,cAAsB;AAC5B,UAAM,aAAa,sBAAsB,SAAS;AAClD,WAAgB,wBAAc,UAAU;AAAA,EAC1C;AACF;","names":["import_observable","decoding"]}
1
+ {"version":3,"sources":["../../src/index.ts","../../src/y-electric.ts","../../src/local-storage-resume-state.ts","../../src/utils.ts"],"sourcesContent":["export * from './y-electric'\nexport * from './local-storage-resume-state'\nexport * from './utils'\nexport * from './types'\n","import * as encoding from 'lib0/encoding'\nimport * as decoding from 'lib0/decoding'\nimport * as awarenessProtocol from 'y-protocols/awareness'\nimport { ObservableV2 } from 'lib0/observable'\nimport * as env from 'lib0/environment'\nimport * as Y from 'yjs'\nimport {\n GetExtensions,\n isChangeMessage,\n isControlMessage,\n Message,\n Offset,\n Row,\n ShapeStream,\n ShapeStreamOptions,\n} from '@electric-sql/client'\nimport {\n YProvider,\n ResumeState,\n SendErrorRetryHandler,\n ElectricProviderOptions,\n} from './types'\n\ntype AwarenessUpdate = {\n added: number[]\n updated: number[]\n removed: number[]\n}\n\nexport class ElectricProvider<\n RowWithDocumentUpdate extends Row<decoding.Decoder> = never,\n RowWithAwarenessUpdate extends Row<decoding.Decoder> = never,\n> extends ObservableV2<YProvider> {\n private doc: Y.Doc\n\n private documentUpdates: {\n shape: ShapeStreamOptions<GetExtensions<RowWithDocumentUpdate>>\n sendUrl: string | URL\n getUpdateFromRow: (row: RowWithDocumentUpdate) => decoding.Decoder\n sendErrorRetryHandler?: SendErrorRetryHandler\n }\n\n private awarenessUpdates?: {\n shape: ShapeStreamOptions<GetExtensions<RowWithAwarenessUpdate>>\n sendUrl: string | URL\n protocol: awarenessProtocol.Awareness\n getUpdateFromRow: (row: RowWithAwarenessUpdate) => decoding.Decoder\n sendErrorRetryHandler?: SendErrorRetryHandler\n }\n\n private _connected: boolean = false\n private _synced: boolean = false\n\n private resumeState: ResumeState\n private sendingPendingChanges: boolean = false\n private pendingChanges: Uint8Array | null = null\n private sendingAwarenessState: boolean = false\n private pendingAwarenessUpdate: AwarenessUpdate | null = null\n private debounceMs: number\n private debounceTimer: ReturnType<typeof setTimeout> | null = null\n\n private documentUpdateHandler: (\n update: Uint8Array,\n origin: unknown,\n doc: Y.Doc,\n transaction: Y.Transaction\n ) => void\n private awarenessUpdateHandler?: (\n update: AwarenessUpdate,\n origin: unknown\n ) => void\n\n private exitHandler: () => void\n private unsubscribeShapes?: () => void\n\n private fetchClient?: typeof fetch\n\n /**\n * Creates a new ElectricProvider instance that connects YJS documents to Electric SQL.\n *\n * @constructor\n * @param {ElectricProviderOptions} options - Configuration options for the provider\n * @param {Y.Doc} options.doc - The YJS document to be synchronized\n * @param {Object} options.documentUpdates - Document updates configuration\n * @param {ShapeStreamOptions} options.documentUpdates.shape - Options for the document updates shape stream\n * @param {string|URL} options.documentUpdates.sendUrl - URL endpoint for sending document updates\n * @param {Function} options.documentUpdates.getUpdateFromRow - Function to extract document update from row\n * @param {SendErrorRetryHandler} [options.documentUpdates.sendErrorRetryHandler] - Error handler for retrying document updates\n * @param {Object} [options.awarenessUpdates] - Awareness updates configuration (optional)\n * @param {ShapeStreamOptions} options.awarenessUpdates.shape - Options for the awareness updates shape stream\n * @param {string|URL} options.awarenessUpdates.sendUrl - URL endpoint for sending awareness updates\n * @param {awarenessProtocol.Awareness} options.awarenessUpdates.protocol - Awareness protocol instance\n * @param {Function} options.awarenessUpdates.getUpdateFromRow - Function to extract awareness update from row\n * @param {SendErrorRetryHandler} [options.awarenessUpdates.sendErrorRetryHandler] - Error handler for retrying awareness updates\n * @param {ResumeState} [options.resumeState] - Resume state for the provider\n * @param {boolean} [options.connect=true] - Whether to automatically connect upon initialization\n * @param {typeof fetch} [options.fetchClient] - Custom fetch implementation to use for HTTP requests\n * @param {number} [options.debounceMs] - Debounce window in milliseconds for sending document updates. If 0 or undefined, debouncing is disabled.\n */\n constructor({\n doc,\n documentUpdates: documentUpdatesConfig,\n awarenessUpdates: awarenessUpdatesConfig,\n resumeState,\n connect = true,\n fetchClient,\n debounceMs,\n }: ElectricProviderOptions<RowWithDocumentUpdate, RowWithAwarenessUpdate>) {\n super()\n\n this.doc = doc\n this.documentUpdates = documentUpdatesConfig\n this.awarenessUpdates = awarenessUpdatesConfig\n this.resumeState = resumeState ?? {}\n this.debounceMs = debounceMs ?? 0\n\n this.fetchClient = fetchClient\n\n this.exitHandler = () => {\n if (env.isNode && typeof process !== `undefined`) {\n process.on(`exit`, this.destroy.bind(this))\n }\n }\n\n this.documentUpdateHandler = this.doc.on(\n `update`,\n this.applyDocumentUpdate.bind(this)\n )\n if (this.awarenessUpdates) {\n this.awarenessUpdateHandler = this.applyAwarenessUpdate.bind(this)\n this.awarenessUpdates.protocol.on(`update`, this.awarenessUpdateHandler!)\n }\n\n // enqueue unsynced changes from document if the\n // resume state provides the document state vector\n if (this.resumeState?.stableStateVector) {\n this.pendingChanges = Y.encodeStateAsUpdate(\n this.doc,\n this.resumeState.stableStateVector\n )\n }\n\n if (connect) {\n this.connect()\n }\n }\n\n get synced() {\n return this._synced\n }\n\n set synced(state) {\n if (this._synced !== state) {\n this._synced = state\n this.emit(`synced`, [state])\n this.emit(`sync`, [state])\n }\n }\n\n set connected(state) {\n if (this._connected !== state) {\n this._connected = state\n if (state) {\n this.sendOperations()\n }\n this.emit(`status`, [{ status: state ? `connected` : `disconnected` }])\n }\n }\n\n get connected() {\n return this._connected\n }\n\n private batch(update: Uint8Array) {\n if (this.pendingChanges) {\n this.pendingChanges = Y.mergeUpdates([this.pendingChanges, update])\n } else {\n this.pendingChanges = update\n }\n }\n\n private clearDebounceTimer() {\n if (this.debounceTimer !== null) {\n clearTimeout(this.debounceTimer)\n this.debounceTimer = null\n }\n }\n\n private scheduleSendOperations() {\n if (this.debounceMs > 0) {\n if (this.debounceTimer === null) {\n this.debounceTimer = setTimeout(async () => {\n this.debounceTimer = null\n await this.sendOperations()\n if (\n this.pendingChanges &&\n this.connected &&\n !this.sendingPendingChanges\n ) {\n this.scheduleSendOperations()\n }\n }, this.debounceMs)\n }\n } else {\n this.sendOperations()\n }\n }\n\n destroy() {\n this.clearDebounceTimer()\n this.disconnect()\n\n this.doc.off(`update`, this.documentUpdateHandler)\n this.awarenessUpdates?.protocol.off(`update`, this.awarenessUpdateHandler!)\n\n if (env.isNode && typeof process !== `undefined`) {\n process.off(`exit`, this.exitHandler!)\n }\n super.destroy()\n }\n\n disconnect() {\n // Flush any pending changes before disconnecting\n this.clearDebounceTimer()\n if (this.pendingChanges && this.connected) {\n this.sendOperations()\n }\n\n this.unsubscribeShapes?.()\n\n if (!this.connected) {\n return\n }\n\n if (this.awarenessUpdates) {\n awarenessProtocol.removeAwarenessStates(\n this.awarenessUpdates.protocol,\n Array.from(this.awarenessUpdates.protocol.getStates().keys()).filter(\n (client) => client !== this.awarenessUpdates!.protocol.clientID\n ),\n this\n )\n\n // try to notifying other clients that we are disconnecting\n awarenessProtocol.removeAwarenessStates(\n this.awarenessUpdates.protocol,\n [this.awarenessUpdates.protocol.clientID],\n `local`\n )\n\n this.awarenessUpdates.protocol.setLocalState({})\n }\n\n // TODO: await for events before closing\n this.emit(`connection-close`, [])\n\n this.pendingAwarenessUpdate = null\n\n this.connected = false\n this.synced = false\n }\n\n connect() {\n if (this.connected) {\n return\n }\n const abortController = new AbortController()\n\n const operationsStream = new ShapeStream<RowWithDocumentUpdate>({\n ...this.documentUpdates.shape,\n ...this.resumeState.document,\n signal: abortController.signal,\n })\n\n const operationsShapeUnsubscribe = operationsStream.subscribe(\n (messages: Message<RowWithDocumentUpdate>[]) => {\n this.operationsShapeHandler(\n messages,\n operationsStream.lastOffset,\n operationsStream.shapeHandle!\n )\n }\n )\n\n let awarenessShapeUnsubscribe: () => void | undefined\n if (this.awarenessUpdates) {\n const awarenessStream = new ShapeStream<RowWithAwarenessUpdate>({\n ...this.awarenessUpdates.shape,\n signal: abortController.signal,\n offset: `now`,\n })\n\n awarenessShapeUnsubscribe = awarenessStream.subscribe(\n (messages: Message<RowWithAwarenessUpdate>[]) => {\n this.awarenessShapeHandler(messages)\n }\n )\n }\n\n this.unsubscribeShapes = () => {\n abortController.abort()\n operationsShapeUnsubscribe()\n awarenessShapeUnsubscribe?.()\n this.unsubscribeShapes = undefined\n }\n\n this.emit(`status`, [{ status: `connecting` }])\n }\n\n private operationsShapeHandler(\n messages: Message<RowWithDocumentUpdate>[],\n offset: Offset,\n handle: string\n ) {\n for (const message of messages) {\n if (isChangeMessage(message)) {\n const decoder = this.documentUpdates.getUpdateFromRow(message.value)\n while (decoder.pos !== decoder.arr.length) {\n const operation = decoding.readVarUint8Array(decoder)\n Y.applyUpdate(this.doc, operation, `server`)\n }\n } else if (\n isControlMessage(message) &&\n message.headers.control === `up-to-date`\n ) {\n this.resumeState.document = {\n offset,\n handle,\n }\n\n if (!this.sendingPendingChanges) {\n this.synced = true\n this.resumeState.stableStateVector = Y.encodeStateVector(this.doc)\n }\n this.emit(`resumeState`, [this.resumeState])\n this.connected = true\n }\n }\n }\n\n private async applyDocumentUpdate(update: Uint8Array, origin: unknown) {\n // don't re-send updates from electric\n if (origin === `server`) {\n return\n }\n\n this.batch(update)\n this.scheduleSendOperations()\n }\n\n private async sendOperations() {\n this.clearDebounceTimer()\n\n if (!this.connected || this.sendingPendingChanges) {\n return\n }\n\n try {\n this.sendingPendingChanges = true\n while (\n this.pendingChanges &&\n this.pendingChanges.length > 2 &&\n this.connected\n ) {\n const sending = this.pendingChanges\n this.pendingChanges = null\n\n const encoder = encoding.createEncoder()\n encoding.writeVarUint8Array(encoder, sending)\n\n const success = await send(\n encoder,\n this.documentUpdates.sendUrl,\n this.fetchClient ?? fetch,\n this.documentUpdates.sendErrorRetryHandler\n )\n if (!success) {\n this.batch(sending)\n this.disconnect()\n }\n }\n // no more pending changes, move stableStateVector forward\n this.resumeState.stableStateVector = Y.encodeStateVector(this.doc)\n this.emit(`resumeState`, [this.resumeState])\n } finally {\n this.sendingPendingChanges = false\n }\n }\n\n private async applyAwarenessUpdate(\n awarenessUpdate: AwarenessUpdate,\n origin: unknown\n ) {\n if (origin !== `local` || !this.connected) {\n return\n }\n\n this.pendingAwarenessUpdate = awarenessUpdate\n\n if (this.sendingAwarenessState) {\n return\n }\n\n this.sendingAwarenessState = true\n\n try {\n while (this.pendingAwarenessUpdate && this.connected) {\n const update = this.pendingAwarenessUpdate\n this.pendingAwarenessUpdate = null\n\n const { added, updated, removed } = update\n const changedClients = added.concat(updated).concat(removed)\n const encoder = encoding.createEncoder()\n\n encoding.writeVarUint8Array(\n encoder,\n awarenessProtocol.encodeAwarenessUpdate(\n this.awarenessUpdates!.protocol,\n changedClients\n )\n )\n const success = await send(\n encoder,\n this.awarenessUpdates!.sendUrl,\n this.fetchClient ?? fetch,\n this.awarenessUpdates!.sendErrorRetryHandler\n )\n if (!success) {\n this.disconnect()\n }\n }\n } finally {\n this.sendingAwarenessState = false\n }\n }\n\n private awarenessShapeHandler(messages: Message<RowWithAwarenessUpdate>[]) {\n for (const message of messages) {\n if (isChangeMessage(message)) {\n if (message.headers.operation === `delete`) {\n awarenessProtocol.removeAwarenessStates(\n this.awarenessUpdates!.protocol,\n [Number(message.value.client_id)],\n `remote`\n )\n } else {\n const decoder = this.awarenessUpdates!.getUpdateFromRow(message.value)\n awarenessProtocol.applyAwarenessUpdate(\n this.awarenessUpdates!.protocol,\n decoding.readVarUint8Array(decoder),\n this\n )\n }\n }\n }\n }\n}\n\nasync function send(\n encoder: encoding.Encoder,\n endpoint: string | URL,\n fetchClient: typeof fetch,\n retryHandler?: SendErrorRetryHandler\n): Promise<boolean> {\n let response: Response | undefined\n const op = encoding.toUint8Array(encoder)\n\n try {\n response = await fetchClient(endpoint!, {\n method: `PUT`,\n headers: {\n 'Content-Type': `application/octet-stream`,\n },\n body: op as BodyInit,\n })\n\n if (!response.ok) {\n throw new Error(`Server did not return 2xx`)\n }\n\n return true\n } catch (error) {\n const shouldRetry = await (retryHandler?.({\n response,\n error,\n }) ?? false)\n return shouldRetry\n }\n}\n","import { ResumeState, ElectricResumeStateProvider } from './types'\nimport { ObservableV2 } from 'lib0/observable.js'\nimport { ElectricProvider } from './y-electric'\nimport * as buffer from 'lib0/buffer'\n\n/**\n * A ResumeStateProvider implementation using LocalStorage.\n * This is a reference implementation that can be used as a starting point\n * for implementing other ResumeStateProviders.\n */\nexport class LocalStorageResumeStateProvider extends ObservableV2<ElectricResumeStateProvider> {\n private key: string\n private resumeState?: ResumeState\n\n constructor(key: string) {\n super()\n this.key = key\n }\n\n subscribeToResumeState(provider: ElectricProvider): () => void {\n const resumeStateHandler = provider.on(`resumeState`, this.save.bind(this))\n return () => provider.off(`resumeState`, resumeStateHandler)\n }\n\n save(resumeState: ResumeState) {\n const jsonPart = JSON.stringify({\n operations: resumeState.document,\n })\n localStorage.setItem(this.key, jsonPart)\n\n if (resumeState.stableStateVector) {\n const vectorBase64 = buffer.toBase64(resumeState.stableStateVector)\n localStorage.setItem(`${this.key}_vector`, vectorBase64)\n } else {\n // ensure vector is removed\n localStorage.removeItem(`${this.key}_vector`)\n }\n }\n\n load(): ResumeState {\n if (this.resumeState) {\n return this.resumeState\n }\n\n const jsonData = localStorage.getItem(this.key)\n if (!jsonData) {\n this.emit(`synced`, [{}])\n } else {\n this.resumeState = JSON.parse(jsonData)\n\n const vectorData = localStorage.getItem(`${this.key}_vector`)\n if (vectorData) {\n this.resumeState!.stableStateVector = buffer.fromBase64(vectorData)\n }\n\n this.emit(`synced`, [this.resumeState!])\n }\n\n return this.resumeState!\n }\n}\n","import * as decoding from 'lib0/decoding'\n\n/**\n * Convert a hex string from PostgreSQL's bytea format to a Uint8Array\n */\nconst hexStringToUint8Array = (hexString: string) => {\n const cleanHexString = hexString.startsWith(`\\\\x`)\n ? hexString.slice(2)\n : hexString\n return new Uint8Array(\n cleanHexString.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16))\n )\n}\n\n/**\n * Utility to parse hex string bytea data to a decoder for YJS operations\n */\nexport const parseToDecoder = {\n bytea: (hexString: string) => {\n const uint8Array = hexStringToUint8Array(hexString)\n return decoding.createDecoder(uint8Array)\n },\n}\n"],"mappings":"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","names":["import_observable","decoding"]}
@@ -39,7 +39,6 @@ type YProvider = {
39
39
  * A resume state provider is used to persist the sync state of a document
40
40
  * This is composed of:
41
41
  * - The document shape offset and handle
42
- * - The awareness shape offset and handle (optional)
43
42
  * - The state vector of the document synced to the server (optional)
44
43
  */
45
44
  type ElectricResumeStateProvider = {
@@ -63,6 +62,7 @@ type ElectricResumeStateProvider = {
63
62
  * @param resumeState (optional) The resume state to use for the provider. If no resume state the provider will fetch the entire shape.
64
63
  * @param connect (optional) Whether to automatically connect upon initialization.
65
64
  * @param fetchClient (optional) Custom fetch implementation to use for send requests.
65
+ * @param debounceMs (optional) Debounce window in milliseconds for sending document updates. If 0 or undefined, debouncing is disabled and updates are sent immediately.
66
66
  */
67
67
  type ElectricProviderOptions<RowWithDocumentUpdate extends Row<decoding.Decoder>, RowWithAwarenessUpdate extends Row<decoding.Decoder> = never> = {
68
68
  doc: Y.Doc;
@@ -82,16 +82,13 @@ type ElectricProviderOptions<RowWithDocumentUpdate extends Row<decoding.Decoder>
82
82
  resumeState?: ResumeState;
83
83
  connect?: boolean;
84
84
  fetchClient?: typeof fetch;
85
+ debounceMs?: number;
85
86
  };
86
87
  type ResumeState = {
87
88
  document?: {
88
89
  offset: Offset;
89
90
  handle: string;
90
91
  };
91
- awareness?: {
92
- offset: Offset;
93
- handle: string;
94
- };
95
92
  stableStateVector?: Uint8Array;
96
93
  };
97
94
 
@@ -106,6 +103,8 @@ declare class ElectricProvider<RowWithDocumentUpdate extends Row<decoding.Decode
106
103
  private pendingChanges;
107
104
  private sendingAwarenessState;
108
105
  private pendingAwarenessUpdate;
106
+ private debounceMs;
107
+ private debounceTimer;
109
108
  private documentUpdateHandler;
110
109
  private awarenessUpdateHandler?;
111
110
  private exitHandler;
@@ -131,13 +130,16 @@ declare class ElectricProvider<RowWithDocumentUpdate extends Row<decoding.Decode
131
130
  * @param {ResumeState} [options.resumeState] - Resume state for the provider
132
131
  * @param {boolean} [options.connect=true] - Whether to automatically connect upon initialization
133
132
  * @param {typeof fetch} [options.fetchClient] - Custom fetch implementation to use for HTTP requests
133
+ * @param {number} [options.debounceMs] - Debounce window in milliseconds for sending document updates. If 0 or undefined, debouncing is disabled.
134
134
  */
135
- constructor({ doc, documentUpdates: documentUpdatesConfig, awarenessUpdates: awarenessUpdatesConfig, resumeState, connect, fetchClient, }: ElectricProviderOptions<RowWithDocumentUpdate, RowWithAwarenessUpdate>);
135
+ constructor({ doc, documentUpdates: documentUpdatesConfig, awarenessUpdates: awarenessUpdatesConfig, resumeState, connect, fetchClient, debounceMs, }: ElectricProviderOptions<RowWithDocumentUpdate, RowWithAwarenessUpdate>);
136
136
  get synced(): boolean;
137
137
  set synced(state: boolean);
138
138
  set connected(state: boolean);
139
139
  get connected(): boolean;
140
140
  private batch;
141
+ private clearDebounceTimer;
142
+ private scheduleSendOperations;
141
143
  destroy(): void;
142
144
  disconnect(): void;
143
145
  connect(): void;
@@ -1,2 +1,2 @@
1
- var P=Object.defineProperty,D=Object.defineProperties;var E=Object.getOwnPropertyDescriptors;var U=Object.getOwnPropertySymbols;var O=Object.prototype.hasOwnProperty,V=Object.prototype.propertyIsEnumerable;var S=(a,t,e)=>t in a?P(a,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[t]=e,l=(a,t)=>{for(var e in t||(t={}))O.call(t,e)&&S(a,e,t[e]);if(U)for(var e of U(t))V.call(t,e)&&S(a,e,t[e]);return a},m=(a,t)=>D(a,E(t));import*as p from"lib0/encoding";import*as f from"lib0/decoding";import*as c from"y-protocols/awareness";import{ObservableV2 as k}from"lib0/observable";import*as w from"lib0/environment";import*as d from"yjs";import{isChangeMessage as y,isControlMessage as v,ShapeStream as b}from"@electric-sql/client";var A=class extends k{constructor({doc:e,documentUpdates:n,awarenessUpdates:o,resumeState:s,connect:r=!0,fetchClient:i}){var h;super();this._connected=!1;this._synced=!1;this.sendingPendingChanges=!1;this.pendingChanges=null;this.sendingAwarenessState=!1;this.pendingAwarenessUpdate=null;this.doc=e,this.documentUpdates=n,this.awarenessUpdates=o,this.resumeState=s!=null?s:{},this.fetchClient=i,this.exitHandler=()=>{w.isNode&&typeof process!="undefined"&&process.on("exit",this.destroy.bind(this))},this.documentUpdateHandler=this.doc.on("update",this.applyDocumentUpdate.bind(this)),this.awarenessUpdates&&(this.awarenessUpdateHandler=this.applyAwarenessUpdate.bind(this),this.awarenessUpdates.protocol.on("update",this.awarenessUpdateHandler)),(h=this.resumeState)!=null&&h.stableStateVector&&(this.pendingChanges=d.encodeStateAsUpdate(this.doc,this.resumeState.stableStateVector)),r&&this.connect()}get synced(){return this._synced}set synced(e){this._synced!==e&&(this._synced=e,this.emit("synced",[e]),this.emit("sync",[e]))}set connected(e){this._connected!==e&&(this._connected=e,e&&this.sendOperations(),this.emit("status",[{status:e?"connected":"disconnected"}]))}get connected(){return this._connected}batch(e){this.pendingChanges?this.pendingChanges=d.mergeUpdates([this.pendingChanges,e]):this.pendingChanges=e}destroy(){var e;this.disconnect(),this.doc.off("update",this.documentUpdateHandler),(e=this.awarenessUpdates)==null||e.protocol.off("update",this.awarenessUpdateHandler),w.isNode&&typeof process!="undefined"&&process.off("exit",this.exitHandler),super.destroy()}disconnect(){var e;(e=this.unsubscribeShapes)==null||e.call(this),this.connected&&(this.awarenessUpdates&&(c.removeAwarenessStates(this.awarenessUpdates.protocol,Array.from(this.awarenessUpdates.protocol.getStates().keys()).filter(n=>n!==this.awarenessUpdates.protocol.clientID),this),c.removeAwarenessStates(this.awarenessUpdates.protocol,[this.awarenessUpdates.protocol.clientID],"local"),this.awarenessUpdates.protocol.setLocalState({})),this.emit("connection-close",[]),this.pendingAwarenessUpdate=null,this.connected=!1,this.synced=!1)}connect(){if(this.connected)return;let e=new AbortController,n=new b(m(l(l({},this.documentUpdates.shape),this.resumeState.document),{signal:e.signal})),o=n.subscribe(r=>{this.operationsShapeHandler(r,n.lastOffset,n.shapeHandle)}),s;if(this.awarenessUpdates){let r=new b(m(l(l({},this.awarenessUpdates.shape),this.resumeState.awareness),{signal:e.signal}));s=r.subscribe(i=>{this.awarenessShapeHandler(i,r.lastOffset,r.shapeHandle)})}this.unsubscribeShapes=()=>{e.abort(),o(),s==null||s(),this.unsubscribeShapes=void 0},this.emit("status",[{status:"connecting"}])}operationsShapeHandler(e,n,o){for(let s of e)if(y(s)){let r=this.documentUpdates.getUpdateFromRow(s.value);for(;r.pos!==r.arr.length;){let i=f.readVarUint8Array(r);d.applyUpdate(this.doc,i,"server")}}else v(s)&&s.headers.control==="up-to-date"&&(this.resumeState.document={offset:n,handle:o},this.sendingPendingChanges||(this.synced=!0,this.resumeState.stableStateVector=d.encodeStateVector(this.doc)),this.emit("resumeState",[this.resumeState]),this.connected=!0)}async applyDocumentUpdate(e,n){n!=="server"&&(this.batch(e),this.sendOperations())}async sendOperations(){var e;if(!(!this.connected||this.sendingPendingChanges))try{for(this.sendingPendingChanges=!0;this.pendingChanges&&this.pendingChanges.length>2&&this.connected;){let n=this.pendingChanges;this.pendingChanges=null;let o=p.createEncoder();p.writeVarUint8Array(o,n),await R(o,this.documentUpdates.sendUrl,(e=this.fetchClient)!=null?e:fetch,this.documentUpdates.sendErrorRetryHandler)||(this.batch(n),this.disconnect())}this.resumeState.stableStateVector=d.encodeStateVector(this.doc),this.emit("resumeState",[this.resumeState])}finally{this.sendingPendingChanges=!1}}async applyAwarenessUpdate(e,n){var o;if(!(n!=="local"||!this.connected)&&(this.pendingAwarenessUpdate=e,!this.sendingAwarenessState)){this.sendingAwarenessState=!0;try{for(;this.pendingAwarenessUpdate&&this.connected;){let s=this.pendingAwarenessUpdate;this.pendingAwarenessUpdate=null;let{added:r,updated:i,removed:h}=s,H=r.concat(i).concat(h),g=p.createEncoder();p.writeVarUint8Array(g,c.encodeAwarenessUpdate(this.awarenessUpdates.protocol,H)),await R(g,this.awarenessUpdates.sendUrl,(o=this.fetchClient)!=null?o:fetch,this.awarenessUpdates.sendErrorRetryHandler)||this.disconnect()}}finally{this.sendingAwarenessState=!1}}}awarenessShapeHandler(e,n,o){for(let s of e)if(y(s))if(s.headers.operation==="delete")c.removeAwarenessStates(this.awarenessUpdates.protocol,[Number(s.value.client_id)],"remote");else{let r=this.awarenessUpdates.getUpdateFromRow(s.value);c.applyAwarenessUpdate(this.awarenessUpdates.protocol,f.readVarUint8Array(r),this)}else v(s)&&s.headers.control==="up-to-date"&&(this.resumeState.awareness={offset:n,handle:o},this.emit("resumeState",[this.resumeState]))}};async function R(a,t,e,n){var r;let o,s=p.toUint8Array(a);try{if(o=await e(t,{method:"PUT",headers:{"Content-Type":"application/octet-stream"},body:s}),!o.ok)throw new Error("Server did not return 2xx");return!0}catch(i){return await((r=n==null?void 0:n({response:o,error:i}))!=null?r:!1)}}import{ObservableV2 as W}from"lib0/observable.js";import*as u from"lib0/buffer";var C=class extends W{constructor(t){super(),this.key=t}subscribeToResumeState(t){let e=t.on("resumeState",this.save.bind(this));return()=>t.off("resumeState",e)}save(t){let e=JSON.stringify({operations:t.document,awareness:t.awareness});if(localStorage.setItem(this.key,e),t.stableStateVector){let n=u.toBase64(t.stableStateVector);localStorage.setItem(`${this.key}_vector`,n)}else localStorage.removeItem(`${this.key}_vector`)}load(){if(this.resumeState)return this.resumeState;let t=localStorage.getItem(this.key);if(!t)this.emit("synced",[{}]);else{this.resumeState=JSON.parse(t);let e=localStorage.getItem(`${this.key}_vector`);e&&(this.resumeState.stableStateVector=u.fromBase64(e)),this.emit("synced",[this.resumeState])}return this.resumeState}};import*as x from"lib0/decoding";var _=a=>{let t=a.startsWith("\\x")?a.slice(2):a;return new Uint8Array(t.match(/.{1,2}/g).map(e=>parseInt(e,16)))},z={bytea:a=>{let t=_(a);return x.createDecoder(t)}};export{A as ElectricProvider,C as LocalStorageResumeStateProvider,z as parseToDecoder};
1
+ var x=Object.defineProperty,P=Object.defineProperties;var H=Object.getOwnPropertyDescriptors;var S=Object.getOwnPropertySymbols;var T=Object.prototype.hasOwnProperty,E=Object.prototype.propertyIsEnumerable;var y=(n,t,e)=>t in n?x(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e,u=(n,t)=>{for(var e in t||(t={}))T.call(t,e)&&y(n,e,t[e]);if(S)for(var e of S(t))E.call(t,e)&&y(n,e,t[e]);return n},g=(n,t)=>P(n,H(t));import*as h from"lib0/encoding";import*as f from"lib0/decoding";import*as c from"y-protocols/awareness";import{ObservableV2 as O}from"lib0/observable";import*as w from"lib0/environment";import*as d from"yjs";import{isChangeMessage as v,isControlMessage as V,ShapeStream as b}from"@electric-sql/client";var A=class extends O{constructor({doc:e,documentUpdates:s,awarenessUpdates:r,resumeState:a,connect:i=!0,fetchClient:o,debounceMs:p}){var l;super();this._connected=!1;this._synced=!1;this.sendingPendingChanges=!1;this.pendingChanges=null;this.sendingAwarenessState=!1;this.pendingAwarenessUpdate=null;this.debounceTimer=null;this.doc=e,this.documentUpdates=s,this.awarenessUpdates=r,this.resumeState=a!=null?a:{},this.debounceMs=p!=null?p:0,this.fetchClient=o,this.exitHandler=()=>{w.isNode&&typeof process!="undefined"&&process.on("exit",this.destroy.bind(this))},this.documentUpdateHandler=this.doc.on("update",this.applyDocumentUpdate.bind(this)),this.awarenessUpdates&&(this.awarenessUpdateHandler=this.applyAwarenessUpdate.bind(this),this.awarenessUpdates.protocol.on("update",this.awarenessUpdateHandler)),(l=this.resumeState)!=null&&l.stableStateVector&&(this.pendingChanges=d.encodeStateAsUpdate(this.doc,this.resumeState.stableStateVector)),i&&this.connect()}get synced(){return this._synced}set synced(e){this._synced!==e&&(this._synced=e,this.emit("synced",[e]),this.emit("sync",[e]))}set connected(e){this._connected!==e&&(this._connected=e,e&&this.sendOperations(),this.emit("status",[{status:e?"connected":"disconnected"}]))}get connected(){return this._connected}batch(e){this.pendingChanges?this.pendingChanges=d.mergeUpdates([this.pendingChanges,e]):this.pendingChanges=e}clearDebounceTimer(){this.debounceTimer!==null&&(clearTimeout(this.debounceTimer),this.debounceTimer=null)}scheduleSendOperations(){this.debounceMs>0?this.debounceTimer===null&&(this.debounceTimer=setTimeout(async()=>{this.debounceTimer=null,await this.sendOperations(),this.pendingChanges&&this.connected&&!this.sendingPendingChanges&&this.scheduleSendOperations()},this.debounceMs)):this.sendOperations()}destroy(){var e;this.clearDebounceTimer(),this.disconnect(),this.doc.off("update",this.documentUpdateHandler),(e=this.awarenessUpdates)==null||e.protocol.off("update",this.awarenessUpdateHandler),w.isNode&&typeof process!="undefined"&&process.off("exit",this.exitHandler),super.destroy()}disconnect(){var e;this.clearDebounceTimer(),this.pendingChanges&&this.connected&&this.sendOperations(),(e=this.unsubscribeShapes)==null||e.call(this),this.connected&&(this.awarenessUpdates&&(c.removeAwarenessStates(this.awarenessUpdates.protocol,Array.from(this.awarenessUpdates.protocol.getStates().keys()).filter(s=>s!==this.awarenessUpdates.protocol.clientID),this),c.removeAwarenessStates(this.awarenessUpdates.protocol,[this.awarenessUpdates.protocol.clientID],"local"),this.awarenessUpdates.protocol.setLocalState({})),this.emit("connection-close",[]),this.pendingAwarenessUpdate=null,this.connected=!1,this.synced=!1)}connect(){if(this.connected)return;let e=new AbortController,s=new b(g(u(u({},this.documentUpdates.shape),this.resumeState.document),{signal:e.signal})),r=s.subscribe(i=>{this.operationsShapeHandler(i,s.lastOffset,s.shapeHandle)}),a;this.awarenessUpdates&&(a=new b(g(u({},this.awarenessUpdates.shape),{signal:e.signal,offset:"now"})).subscribe(o=>{this.awarenessShapeHandler(o)})),this.unsubscribeShapes=()=>{e.abort(),r(),a==null||a(),this.unsubscribeShapes=void 0},this.emit("status",[{status:"connecting"}])}operationsShapeHandler(e,s,r){for(let a of e)if(v(a)){let i=this.documentUpdates.getUpdateFromRow(a.value);for(;i.pos!==i.arr.length;){let o=f.readVarUint8Array(i);d.applyUpdate(this.doc,o,"server")}}else V(a)&&a.headers.control==="up-to-date"&&(this.resumeState.document={offset:s,handle:r},this.sendingPendingChanges||(this.synced=!0,this.resumeState.stableStateVector=d.encodeStateVector(this.doc)),this.emit("resumeState",[this.resumeState]),this.connected=!0)}async applyDocumentUpdate(e,s){s!=="server"&&(this.batch(e),this.scheduleSendOperations())}async sendOperations(){var e;if(this.clearDebounceTimer(),!(!this.connected||this.sendingPendingChanges))try{for(this.sendingPendingChanges=!0;this.pendingChanges&&this.pendingChanges.length>2&&this.connected;){let s=this.pendingChanges;this.pendingChanges=null;let r=h.createEncoder();h.writeVarUint8Array(r,s),await R(r,this.documentUpdates.sendUrl,(e=this.fetchClient)!=null?e:fetch,this.documentUpdates.sendErrorRetryHandler)||(this.batch(s),this.disconnect())}this.resumeState.stableStateVector=d.encodeStateVector(this.doc),this.emit("resumeState",[this.resumeState])}finally{this.sendingPendingChanges=!1}}async applyAwarenessUpdate(e,s){var r;if(!(s!=="local"||!this.connected)&&(this.pendingAwarenessUpdate=e,!this.sendingAwarenessState)){this.sendingAwarenessState=!0;try{for(;this.pendingAwarenessUpdate&&this.connected;){let a=this.pendingAwarenessUpdate;this.pendingAwarenessUpdate=null;let{added:i,updated:o,removed:p}=a,l=i.concat(o).concat(p),U=h.createEncoder();h.writeVarUint8Array(U,c.encodeAwarenessUpdate(this.awarenessUpdates.protocol,l)),await R(U,this.awarenessUpdates.sendUrl,(r=this.fetchClient)!=null?r:fetch,this.awarenessUpdates.sendErrorRetryHandler)||this.disconnect()}}finally{this.sendingAwarenessState=!1}}}awarenessShapeHandler(e){for(let s of e)if(v(s))if(s.headers.operation==="delete")c.removeAwarenessStates(this.awarenessUpdates.protocol,[Number(s.value.client_id)],"remote");else{let r=this.awarenessUpdates.getUpdateFromRow(s.value);c.applyAwarenessUpdate(this.awarenessUpdates.protocol,f.readVarUint8Array(r),this)}}};async function R(n,t,e,s){var i;let r,a=h.toUint8Array(n);try{if(r=await e(t,{method:"PUT",headers:{"Content-Type":"application/octet-stream"},body:a}),!r.ok)throw new Error("Server did not return 2xx");return!0}catch(o){return await((i=s==null?void 0:s({response:r,error:o}))!=null?i:!1)}}import{ObservableV2 as W}from"lib0/observable.js";import*as m from"lib0/buffer";var C=class extends W{constructor(t){super(),this.key=t}subscribeToResumeState(t){let e=t.on("resumeState",this.save.bind(this));return()=>t.off("resumeState",e)}save(t){let e=JSON.stringify({operations:t.document});if(localStorage.setItem(this.key,e),t.stableStateVector){let s=m.toBase64(t.stableStateVector);localStorage.setItem(`${this.key}_vector`,s)}else localStorage.removeItem(`${this.key}_vector`)}load(){if(this.resumeState)return this.resumeState;let t=localStorage.getItem(this.key);if(!t)this.emit("synced",[{}]);else{this.resumeState=JSON.parse(t);let e=localStorage.getItem(`${this.key}_vector`);e&&(this.resumeState.stableStateVector=m.fromBase64(e)),this.emit("synced",[this.resumeState])}return this.resumeState}};import*as D from"lib0/decoding";var k=n=>{let t=n.startsWith("\\x")?n.slice(2):n;return new Uint8Array(t.match(/.{1,2}/g).map(e=>parseInt(e,16)))},z={bytea:n=>{let t=k(n);return D.createDecoder(t)}};export{A as ElectricProvider,C as LocalStorageResumeStateProvider,z as parseToDecoder};
2
2
  //# sourceMappingURL=index.browser.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/y-electric.ts","../src/local-storage-resume-state.ts","../src/utils.ts"],"sourcesContent":["import * as encoding from 'lib0/encoding'\nimport * as decoding from 'lib0/decoding'\nimport * as awarenessProtocol from 'y-protocols/awareness'\nimport { ObservableV2 } from 'lib0/observable'\nimport * as env from 'lib0/environment'\nimport * as Y from 'yjs'\nimport {\n GetExtensions,\n isChangeMessage,\n isControlMessage,\n Message,\n Offset,\n Row,\n ShapeStream,\n ShapeStreamOptions,\n} from '@electric-sql/client'\nimport {\n YProvider,\n ResumeState,\n SendErrorRetryHandler,\n ElectricProviderOptions,\n} from './types'\n\ntype AwarenessUpdate = {\n added: number[]\n updated: number[]\n removed: number[]\n}\n\nexport class ElectricProvider<\n RowWithDocumentUpdate extends Row<decoding.Decoder> = never,\n RowWithAwarenessUpdate extends Row<decoding.Decoder> = never,\n> extends ObservableV2<YProvider> {\n private doc: Y.Doc\n\n private documentUpdates: {\n shape: ShapeStreamOptions<GetExtensions<RowWithDocumentUpdate>>\n sendUrl: string | URL\n getUpdateFromRow: (row: RowWithDocumentUpdate) => decoding.Decoder\n sendErrorRetryHandler?: SendErrorRetryHandler\n }\n\n private awarenessUpdates?: {\n shape: ShapeStreamOptions<GetExtensions<RowWithAwarenessUpdate>>\n sendUrl: string | URL\n protocol: awarenessProtocol.Awareness\n getUpdateFromRow: (row: RowWithAwarenessUpdate) => decoding.Decoder\n sendErrorRetryHandler?: SendErrorRetryHandler\n }\n\n private _connected: boolean = false\n private _synced: boolean = false\n\n private resumeState: ResumeState\n private sendingPendingChanges: boolean = false\n private pendingChanges: Uint8Array | null = null\n private sendingAwarenessState: boolean = false\n private pendingAwarenessUpdate: AwarenessUpdate | null = null\n\n private documentUpdateHandler: (\n update: Uint8Array,\n origin: unknown,\n doc: Y.Doc,\n transaction: Y.Transaction\n ) => void\n private awarenessUpdateHandler?: (\n update: AwarenessUpdate,\n origin: unknown\n ) => void\n\n private exitHandler: () => void\n private unsubscribeShapes?: () => void\n\n private fetchClient?: typeof fetch\n\n /**\n * Creates a new ElectricProvider instance that connects YJS documents to Electric SQL.\n *\n * @constructor\n * @param {ElectricProviderOptions} options - Configuration options for the provider\n * @param {Y.Doc} options.doc - The YJS document to be synchronized\n * @param {Object} options.documentUpdates - Document updates configuration\n * @param {ShapeStreamOptions} options.documentUpdates.shape - Options for the document updates shape stream\n * @param {string|URL} options.documentUpdates.sendUrl - URL endpoint for sending document updates\n * @param {Function} options.documentUpdates.getUpdateFromRow - Function to extract document update from row\n * @param {SendErrorRetryHandler} [options.documentUpdates.sendErrorRetryHandler] - Error handler for retrying document updates\n * @param {Object} [options.awarenessUpdates] - Awareness updates configuration (optional)\n * @param {ShapeStreamOptions} options.awarenessUpdates.shape - Options for the awareness updates shape stream\n * @param {string|URL} options.awarenessUpdates.sendUrl - URL endpoint for sending awareness updates\n * @param {awarenessProtocol.Awareness} options.awarenessUpdates.protocol - Awareness protocol instance\n * @param {Function} options.awarenessUpdates.getUpdateFromRow - Function to extract awareness update from row\n * @param {SendErrorRetryHandler} [options.awarenessUpdates.sendErrorRetryHandler] - Error handler for retrying awareness updates\n * @param {ResumeState} [options.resumeState] - Resume state for the provider\n * @param {boolean} [options.connect=true] - Whether to automatically connect upon initialization\n * @param {typeof fetch} [options.fetchClient] - Custom fetch implementation to use for HTTP requests\n */\n constructor({\n doc,\n documentUpdates: documentUpdatesConfig,\n awarenessUpdates: awarenessUpdatesConfig,\n resumeState,\n connect = true,\n fetchClient,\n }: ElectricProviderOptions<RowWithDocumentUpdate, RowWithAwarenessUpdate>) {\n super()\n\n this.doc = doc\n this.documentUpdates = documentUpdatesConfig\n this.awarenessUpdates = awarenessUpdatesConfig\n this.resumeState = resumeState ?? {}\n\n this.fetchClient = fetchClient\n\n this.exitHandler = () => {\n if (env.isNode && typeof process !== `undefined`) {\n process.on(`exit`, this.destroy.bind(this))\n }\n }\n\n this.documentUpdateHandler = this.doc.on(\n `update`,\n this.applyDocumentUpdate.bind(this)\n )\n if (this.awarenessUpdates) {\n this.awarenessUpdateHandler = this.applyAwarenessUpdate.bind(this)\n this.awarenessUpdates.protocol.on(`update`, this.awarenessUpdateHandler!)\n }\n\n // enqueue unsynced changes from document if the\n // resume state provides the document state vector\n if (this.resumeState?.stableStateVector) {\n this.pendingChanges = Y.encodeStateAsUpdate(\n this.doc,\n this.resumeState.stableStateVector\n )\n }\n\n if (connect) {\n this.connect()\n }\n }\n\n get synced() {\n return this._synced\n }\n\n set synced(state) {\n if (this._synced !== state) {\n this._synced = state\n this.emit(`synced`, [state])\n this.emit(`sync`, [state])\n }\n }\n\n set connected(state) {\n if (this._connected !== state) {\n this._connected = state\n if (state) {\n this.sendOperations()\n }\n this.emit(`status`, [{ status: state ? `connected` : `disconnected` }])\n }\n }\n\n get connected() {\n return this._connected\n }\n\n private batch(update: Uint8Array) {\n if (this.pendingChanges) {\n this.pendingChanges = Y.mergeUpdates([this.pendingChanges, update])\n } else {\n this.pendingChanges = update\n }\n }\n\n destroy() {\n this.disconnect()\n\n this.doc.off(`update`, this.documentUpdateHandler)\n this.awarenessUpdates?.protocol.off(`update`, this.awarenessUpdateHandler!)\n\n if (env.isNode && typeof process !== `undefined`) {\n process.off(`exit`, this.exitHandler!)\n }\n super.destroy()\n }\n\n disconnect() {\n this.unsubscribeShapes?.()\n\n if (!this.connected) {\n return\n }\n\n if (this.awarenessUpdates) {\n awarenessProtocol.removeAwarenessStates(\n this.awarenessUpdates.protocol,\n Array.from(this.awarenessUpdates.protocol.getStates().keys()).filter(\n (client) => client !== this.awarenessUpdates!.protocol.clientID\n ),\n this\n )\n\n // try to notifying other clients that we are disconnecting\n awarenessProtocol.removeAwarenessStates(\n this.awarenessUpdates.protocol,\n [this.awarenessUpdates.protocol.clientID],\n `local`\n )\n\n this.awarenessUpdates.protocol.setLocalState({})\n }\n\n // TODO: await for events before closing\n this.emit(`connection-close`, [])\n\n this.pendingAwarenessUpdate = null\n\n this.connected = false\n this.synced = false\n }\n\n connect() {\n if (this.connected) {\n return\n }\n const abortController = new AbortController()\n\n const operationsStream = new ShapeStream<RowWithDocumentUpdate>({\n ...this.documentUpdates.shape,\n ...this.resumeState.document,\n signal: abortController.signal,\n })\n\n const operationsShapeUnsubscribe = operationsStream.subscribe(\n (messages) => {\n this.operationsShapeHandler(\n messages,\n operationsStream.lastOffset,\n operationsStream.shapeHandle!\n )\n }\n )\n\n let awarenessShapeUnsubscribe: () => void | undefined\n if (this.awarenessUpdates) {\n const awarenessStream = new ShapeStream<RowWithAwarenessUpdate>({\n ...this.awarenessUpdates.shape,\n ...this.resumeState.awareness,\n signal: abortController.signal,\n })\n\n awarenessShapeUnsubscribe = awarenessStream.subscribe((messages) => {\n this.awarenessShapeHandler(\n messages,\n awarenessStream.lastOffset,\n awarenessStream.shapeHandle!\n )\n })\n }\n\n this.unsubscribeShapes = () => {\n abortController.abort()\n operationsShapeUnsubscribe()\n awarenessShapeUnsubscribe?.()\n this.unsubscribeShapes = undefined\n }\n\n this.emit(`status`, [{ status: `connecting` }])\n }\n\n private operationsShapeHandler(\n messages: Message<RowWithDocumentUpdate>[],\n offset: Offset,\n handle: string\n ) {\n for (const message of messages) {\n if (isChangeMessage(message)) {\n const decoder = this.documentUpdates.getUpdateFromRow(message.value)\n while (decoder.pos !== decoder.arr.length) {\n const operation = decoding.readVarUint8Array(decoder)\n Y.applyUpdate(this.doc, operation, `server`)\n }\n } else if (\n isControlMessage(message) &&\n message.headers.control === `up-to-date`\n ) {\n this.resumeState.document = {\n offset,\n handle,\n }\n\n if (!this.sendingPendingChanges) {\n this.synced = true\n this.resumeState.stableStateVector = Y.encodeStateVector(this.doc)\n }\n this.emit(`resumeState`, [this.resumeState])\n this.connected = true\n }\n }\n }\n\n // TODO: add an optional throttler that batches updates\n // before pushing to the server\n private async applyDocumentUpdate(update: Uint8Array, origin: unknown) {\n // don't re-send updates from electric\n if (origin === `server`) {\n return\n }\n\n this.batch(update)\n this.sendOperations()\n }\n\n private async sendOperations() {\n if (!this.connected || this.sendingPendingChanges) {\n return\n }\n\n try {\n this.sendingPendingChanges = true\n while (\n this.pendingChanges &&\n this.pendingChanges.length > 2 &&\n this.connected\n ) {\n const sending = this.pendingChanges\n this.pendingChanges = null\n\n const encoder = encoding.createEncoder()\n encoding.writeVarUint8Array(encoder, sending)\n\n const success = await send(\n encoder,\n this.documentUpdates.sendUrl,\n this.fetchClient ?? fetch,\n this.documentUpdates.sendErrorRetryHandler\n )\n if (!success) {\n this.batch(sending)\n this.disconnect()\n }\n }\n // no more pending changes, move stableStateVector forward\n this.resumeState.stableStateVector = Y.encodeStateVector(this.doc)\n this.emit(`resumeState`, [this.resumeState])\n } finally {\n this.sendingPendingChanges = false\n }\n }\n\n private async applyAwarenessUpdate(\n awarenessUpdate: AwarenessUpdate,\n origin: unknown\n ) {\n if (origin !== `local` || !this.connected) {\n return\n }\n\n this.pendingAwarenessUpdate = awarenessUpdate\n\n if (this.sendingAwarenessState) {\n return\n }\n\n this.sendingAwarenessState = true\n\n try {\n while (this.pendingAwarenessUpdate && this.connected) {\n const update = this.pendingAwarenessUpdate\n this.pendingAwarenessUpdate = null\n\n const { added, updated, removed } = update\n const changedClients = added.concat(updated).concat(removed)\n const encoder = encoding.createEncoder()\n\n encoding.writeVarUint8Array(\n encoder,\n awarenessProtocol.encodeAwarenessUpdate(\n this.awarenessUpdates!.protocol,\n changedClients\n )\n )\n const success = await send(\n encoder,\n this.awarenessUpdates!.sendUrl,\n this.fetchClient ?? fetch,\n this.awarenessUpdates!.sendErrorRetryHandler\n )\n if (!success) {\n this.disconnect()\n }\n }\n } finally {\n this.sendingAwarenessState = false\n }\n }\n\n private awarenessShapeHandler(\n messages: Message<RowWithAwarenessUpdate>[],\n offset: Offset,\n handle: string\n ) {\n for (const message of messages) {\n if (isChangeMessage(message)) {\n if (message.headers.operation === `delete`) {\n awarenessProtocol.removeAwarenessStates(\n this.awarenessUpdates!.protocol,\n [Number(message.value.client_id)],\n `remote`\n )\n } else {\n const decoder = this.awarenessUpdates!.getUpdateFromRow(message.value)\n awarenessProtocol.applyAwarenessUpdate(\n this.awarenessUpdates!.protocol,\n decoding.readVarUint8Array(decoder),\n this\n )\n }\n } else if (\n isControlMessage(message) &&\n message.headers.control === `up-to-date`\n ) {\n this.resumeState.awareness = {\n offset: offset,\n handle: handle,\n }\n this.emit(`resumeState`, [this.resumeState])\n }\n }\n }\n}\n\nasync function send(\n encoder: encoding.Encoder,\n endpoint: string | URL,\n fetchClient: typeof fetch,\n retryHandler?: SendErrorRetryHandler\n): Promise<boolean> {\n let response: Response | undefined\n const op = encoding.toUint8Array(encoder)\n\n try {\n response = await fetchClient(endpoint!, {\n method: `PUT`,\n headers: {\n 'Content-Type': `application/octet-stream`,\n },\n body: op as BodyInit,\n })\n\n if (!response.ok) {\n throw new Error(`Server did not return 2xx`)\n }\n\n return true\n } catch (error) {\n const shouldRetry = await (retryHandler?.({\n response,\n error,\n }) ?? false)\n return shouldRetry\n }\n}\n","import { ResumeState, ElectricResumeStateProvider } from './types'\nimport { ObservableV2 } from 'lib0/observable.js'\nimport { ElectricProvider } from './y-electric'\nimport * as buffer from 'lib0/buffer'\n\n/**\n * A ResumeStateProvider implementation using LocalStorage.\n * This is a reference implementation that can be used as a starting point\n * for implementing other ResumeStateProviders.\n */\nexport class LocalStorageResumeStateProvider extends ObservableV2<ElectricResumeStateProvider> {\n private key: string\n private resumeState?: ResumeState\n\n constructor(key: string) {\n super()\n this.key = key\n }\n\n subscribeToResumeState(provider: ElectricProvider): () => void {\n const resumeStateHandler = provider.on(`resumeState`, this.save.bind(this))\n return () => provider.off(`resumeState`, resumeStateHandler)\n }\n\n save(resumeState: ResumeState) {\n const jsonPart = JSON.stringify({\n operations: resumeState.document,\n awareness: resumeState.awareness,\n })\n localStorage.setItem(this.key, jsonPart)\n\n if (resumeState.stableStateVector) {\n const vectorBase64 = buffer.toBase64(resumeState.stableStateVector)\n localStorage.setItem(`${this.key}_vector`, vectorBase64)\n } else {\n // ensure vector is removed\n localStorage.removeItem(`${this.key}_vector`)\n }\n }\n\n load(): ResumeState {\n if (this.resumeState) {\n return this.resumeState\n }\n\n const jsonData = localStorage.getItem(this.key)\n if (!jsonData) {\n this.emit(`synced`, [{}])\n } else {\n this.resumeState = JSON.parse(jsonData)\n\n const vectorData = localStorage.getItem(`${this.key}_vector`)\n if (vectorData) {\n this.resumeState!.stableStateVector = buffer.fromBase64(vectorData)\n }\n\n this.emit(`synced`, [this.resumeState!])\n }\n\n return this.resumeState!\n }\n}\n","import * as decoding from 'lib0/decoding'\n\n/**\n * Convert a hex string from PostgreSQL's bytea format to a Uint8Array\n */\nconst hexStringToUint8Array = (hexString: string) => {\n const cleanHexString = hexString.startsWith(`\\\\x`)\n ? hexString.slice(2)\n : hexString\n return new Uint8Array(\n cleanHexString.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16))\n )\n}\n\n/**\n * Utility to parse hex string bytea data to a decoder for YJS operations\n */\nexport const parseToDecoder = {\n bytea: (hexString: string) => {\n const uint8Array = hexStringToUint8Array(hexString)\n return decoding.createDecoder(uint8Array)\n },\n}\n"],"mappings":"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","names":["encoding","decoding","awarenessProtocol","ObservableV2","env","Y","isChangeMessage","isControlMessage","ShapeStream","ElectricProvider","ObservableV2","doc","documentUpdatesConfig","awarenessUpdatesConfig","resumeState","connect","fetchClient","_a","state","update","client","abortController","operationsStream","ShapeStream","__spreadProps","__spreadValues","operationsShapeUnsubscribe","messages","awarenessShapeUnsubscribe","awarenessStream","offset","handle","message","isChangeMessage","decoder","operation","isControlMessage","origin","sending","encoder","send","awarenessUpdate","added","updated","removed","changedClients","endpoint","retryHandler","response","op","error","ObservableV2","buffer","LocalStorageResumeStateProvider","key","provider","resumeStateHandler","resumeState","jsonPart","vectorBase64","jsonData","vectorData","decoding","hexStringToUint8Array","hexString","cleanHexString","byte","parseToDecoder","uint8Array"]}
1
+ {"version":3,"sources":["../src/y-electric.ts","../src/local-storage-resume-state.ts","../src/utils.ts"],"sourcesContent":["import * as encoding from 'lib0/encoding'\nimport * as decoding from 'lib0/decoding'\nimport * as awarenessProtocol from 'y-protocols/awareness'\nimport { ObservableV2 } from 'lib0/observable'\nimport * as env from 'lib0/environment'\nimport * as Y from 'yjs'\nimport {\n GetExtensions,\n isChangeMessage,\n isControlMessage,\n Message,\n Offset,\n Row,\n ShapeStream,\n ShapeStreamOptions,\n} from '@electric-sql/client'\nimport {\n YProvider,\n ResumeState,\n SendErrorRetryHandler,\n ElectricProviderOptions,\n} from './types'\n\ntype AwarenessUpdate = {\n added: number[]\n updated: number[]\n removed: number[]\n}\n\nexport class ElectricProvider<\n RowWithDocumentUpdate extends Row<decoding.Decoder> = never,\n RowWithAwarenessUpdate extends Row<decoding.Decoder> = never,\n> extends ObservableV2<YProvider> {\n private doc: Y.Doc\n\n private documentUpdates: {\n shape: ShapeStreamOptions<GetExtensions<RowWithDocumentUpdate>>\n sendUrl: string | URL\n getUpdateFromRow: (row: RowWithDocumentUpdate) => decoding.Decoder\n sendErrorRetryHandler?: SendErrorRetryHandler\n }\n\n private awarenessUpdates?: {\n shape: ShapeStreamOptions<GetExtensions<RowWithAwarenessUpdate>>\n sendUrl: string | URL\n protocol: awarenessProtocol.Awareness\n getUpdateFromRow: (row: RowWithAwarenessUpdate) => decoding.Decoder\n sendErrorRetryHandler?: SendErrorRetryHandler\n }\n\n private _connected: boolean = false\n private _synced: boolean = false\n\n private resumeState: ResumeState\n private sendingPendingChanges: boolean = false\n private pendingChanges: Uint8Array | null = null\n private sendingAwarenessState: boolean = false\n private pendingAwarenessUpdate: AwarenessUpdate | null = null\n private debounceMs: number\n private debounceTimer: ReturnType<typeof setTimeout> | null = null\n\n private documentUpdateHandler: (\n update: Uint8Array,\n origin: unknown,\n doc: Y.Doc,\n transaction: Y.Transaction\n ) => void\n private awarenessUpdateHandler?: (\n update: AwarenessUpdate,\n origin: unknown\n ) => void\n\n private exitHandler: () => void\n private unsubscribeShapes?: () => void\n\n private fetchClient?: typeof fetch\n\n /**\n * Creates a new ElectricProvider instance that connects YJS documents to Electric SQL.\n *\n * @constructor\n * @param {ElectricProviderOptions} options - Configuration options for the provider\n * @param {Y.Doc} options.doc - The YJS document to be synchronized\n * @param {Object} options.documentUpdates - Document updates configuration\n * @param {ShapeStreamOptions} options.documentUpdates.shape - Options for the document updates shape stream\n * @param {string|URL} options.documentUpdates.sendUrl - URL endpoint for sending document updates\n * @param {Function} options.documentUpdates.getUpdateFromRow - Function to extract document update from row\n * @param {SendErrorRetryHandler} [options.documentUpdates.sendErrorRetryHandler] - Error handler for retrying document updates\n * @param {Object} [options.awarenessUpdates] - Awareness updates configuration (optional)\n * @param {ShapeStreamOptions} options.awarenessUpdates.shape - Options for the awareness updates shape stream\n * @param {string|URL} options.awarenessUpdates.sendUrl - URL endpoint for sending awareness updates\n * @param {awarenessProtocol.Awareness} options.awarenessUpdates.protocol - Awareness protocol instance\n * @param {Function} options.awarenessUpdates.getUpdateFromRow - Function to extract awareness update from row\n * @param {SendErrorRetryHandler} [options.awarenessUpdates.sendErrorRetryHandler] - Error handler for retrying awareness updates\n * @param {ResumeState} [options.resumeState] - Resume state for the provider\n * @param {boolean} [options.connect=true] - Whether to automatically connect upon initialization\n * @param {typeof fetch} [options.fetchClient] - Custom fetch implementation to use for HTTP requests\n * @param {number} [options.debounceMs] - Debounce window in milliseconds for sending document updates. If 0 or undefined, debouncing is disabled.\n */\n constructor({\n doc,\n documentUpdates: documentUpdatesConfig,\n awarenessUpdates: awarenessUpdatesConfig,\n resumeState,\n connect = true,\n fetchClient,\n debounceMs,\n }: ElectricProviderOptions<RowWithDocumentUpdate, RowWithAwarenessUpdate>) {\n super()\n\n this.doc = doc\n this.documentUpdates = documentUpdatesConfig\n this.awarenessUpdates = awarenessUpdatesConfig\n this.resumeState = resumeState ?? {}\n this.debounceMs = debounceMs ?? 0\n\n this.fetchClient = fetchClient\n\n this.exitHandler = () => {\n if (env.isNode && typeof process !== `undefined`) {\n process.on(`exit`, this.destroy.bind(this))\n }\n }\n\n this.documentUpdateHandler = this.doc.on(\n `update`,\n this.applyDocumentUpdate.bind(this)\n )\n if (this.awarenessUpdates) {\n this.awarenessUpdateHandler = this.applyAwarenessUpdate.bind(this)\n this.awarenessUpdates.protocol.on(`update`, this.awarenessUpdateHandler!)\n }\n\n // enqueue unsynced changes from document if the\n // resume state provides the document state vector\n if (this.resumeState?.stableStateVector) {\n this.pendingChanges = Y.encodeStateAsUpdate(\n this.doc,\n this.resumeState.stableStateVector\n )\n }\n\n if (connect) {\n this.connect()\n }\n }\n\n get synced() {\n return this._synced\n }\n\n set synced(state) {\n if (this._synced !== state) {\n this._synced = state\n this.emit(`synced`, [state])\n this.emit(`sync`, [state])\n }\n }\n\n set connected(state) {\n if (this._connected !== state) {\n this._connected = state\n if (state) {\n this.sendOperations()\n }\n this.emit(`status`, [{ status: state ? `connected` : `disconnected` }])\n }\n }\n\n get connected() {\n return this._connected\n }\n\n private batch(update: Uint8Array) {\n if (this.pendingChanges) {\n this.pendingChanges = Y.mergeUpdates([this.pendingChanges, update])\n } else {\n this.pendingChanges = update\n }\n }\n\n private clearDebounceTimer() {\n if (this.debounceTimer !== null) {\n clearTimeout(this.debounceTimer)\n this.debounceTimer = null\n }\n }\n\n private scheduleSendOperations() {\n if (this.debounceMs > 0) {\n if (this.debounceTimer === null) {\n this.debounceTimer = setTimeout(async () => {\n this.debounceTimer = null\n await this.sendOperations()\n if (\n this.pendingChanges &&\n this.connected &&\n !this.sendingPendingChanges\n ) {\n this.scheduleSendOperations()\n }\n }, this.debounceMs)\n }\n } else {\n this.sendOperations()\n }\n }\n\n destroy() {\n this.clearDebounceTimer()\n this.disconnect()\n\n this.doc.off(`update`, this.documentUpdateHandler)\n this.awarenessUpdates?.protocol.off(`update`, this.awarenessUpdateHandler!)\n\n if (env.isNode && typeof process !== `undefined`) {\n process.off(`exit`, this.exitHandler!)\n }\n super.destroy()\n }\n\n disconnect() {\n // Flush any pending changes before disconnecting\n this.clearDebounceTimer()\n if (this.pendingChanges && this.connected) {\n this.sendOperations()\n }\n\n this.unsubscribeShapes?.()\n\n if (!this.connected) {\n return\n }\n\n if (this.awarenessUpdates) {\n awarenessProtocol.removeAwarenessStates(\n this.awarenessUpdates.protocol,\n Array.from(this.awarenessUpdates.protocol.getStates().keys()).filter(\n (client) => client !== this.awarenessUpdates!.protocol.clientID\n ),\n this\n )\n\n // try to notifying other clients that we are disconnecting\n awarenessProtocol.removeAwarenessStates(\n this.awarenessUpdates.protocol,\n [this.awarenessUpdates.protocol.clientID],\n `local`\n )\n\n this.awarenessUpdates.protocol.setLocalState({})\n }\n\n // TODO: await for events before closing\n this.emit(`connection-close`, [])\n\n this.pendingAwarenessUpdate = null\n\n this.connected = false\n this.synced = false\n }\n\n connect() {\n if (this.connected) {\n return\n }\n const abortController = new AbortController()\n\n const operationsStream = new ShapeStream<RowWithDocumentUpdate>({\n ...this.documentUpdates.shape,\n ...this.resumeState.document,\n signal: abortController.signal,\n })\n\n const operationsShapeUnsubscribe = operationsStream.subscribe(\n (messages: Message<RowWithDocumentUpdate>[]) => {\n this.operationsShapeHandler(\n messages,\n operationsStream.lastOffset,\n operationsStream.shapeHandle!\n )\n }\n )\n\n let awarenessShapeUnsubscribe: () => void | undefined\n if (this.awarenessUpdates) {\n const awarenessStream = new ShapeStream<RowWithAwarenessUpdate>({\n ...this.awarenessUpdates.shape,\n signal: abortController.signal,\n offset: `now`,\n })\n\n awarenessShapeUnsubscribe = awarenessStream.subscribe(\n (messages: Message<RowWithAwarenessUpdate>[]) => {\n this.awarenessShapeHandler(messages)\n }\n )\n }\n\n this.unsubscribeShapes = () => {\n abortController.abort()\n operationsShapeUnsubscribe()\n awarenessShapeUnsubscribe?.()\n this.unsubscribeShapes = undefined\n }\n\n this.emit(`status`, [{ status: `connecting` }])\n }\n\n private operationsShapeHandler(\n messages: Message<RowWithDocumentUpdate>[],\n offset: Offset,\n handle: string\n ) {\n for (const message of messages) {\n if (isChangeMessage(message)) {\n const decoder = this.documentUpdates.getUpdateFromRow(message.value)\n while (decoder.pos !== decoder.arr.length) {\n const operation = decoding.readVarUint8Array(decoder)\n Y.applyUpdate(this.doc, operation, `server`)\n }\n } else if (\n isControlMessage(message) &&\n message.headers.control === `up-to-date`\n ) {\n this.resumeState.document = {\n offset,\n handle,\n }\n\n if (!this.sendingPendingChanges) {\n this.synced = true\n this.resumeState.stableStateVector = Y.encodeStateVector(this.doc)\n }\n this.emit(`resumeState`, [this.resumeState])\n this.connected = true\n }\n }\n }\n\n private async applyDocumentUpdate(update: Uint8Array, origin: unknown) {\n // don't re-send updates from electric\n if (origin === `server`) {\n return\n }\n\n this.batch(update)\n this.scheduleSendOperations()\n }\n\n private async sendOperations() {\n this.clearDebounceTimer()\n\n if (!this.connected || this.sendingPendingChanges) {\n return\n }\n\n try {\n this.sendingPendingChanges = true\n while (\n this.pendingChanges &&\n this.pendingChanges.length > 2 &&\n this.connected\n ) {\n const sending = this.pendingChanges\n this.pendingChanges = null\n\n const encoder = encoding.createEncoder()\n encoding.writeVarUint8Array(encoder, sending)\n\n const success = await send(\n encoder,\n this.documentUpdates.sendUrl,\n this.fetchClient ?? fetch,\n this.documentUpdates.sendErrorRetryHandler\n )\n if (!success) {\n this.batch(sending)\n this.disconnect()\n }\n }\n // no more pending changes, move stableStateVector forward\n this.resumeState.stableStateVector = Y.encodeStateVector(this.doc)\n this.emit(`resumeState`, [this.resumeState])\n } finally {\n this.sendingPendingChanges = false\n }\n }\n\n private async applyAwarenessUpdate(\n awarenessUpdate: AwarenessUpdate,\n origin: unknown\n ) {\n if (origin !== `local` || !this.connected) {\n return\n }\n\n this.pendingAwarenessUpdate = awarenessUpdate\n\n if (this.sendingAwarenessState) {\n return\n }\n\n this.sendingAwarenessState = true\n\n try {\n while (this.pendingAwarenessUpdate && this.connected) {\n const update = this.pendingAwarenessUpdate\n this.pendingAwarenessUpdate = null\n\n const { added, updated, removed } = update\n const changedClients = added.concat(updated).concat(removed)\n const encoder = encoding.createEncoder()\n\n encoding.writeVarUint8Array(\n encoder,\n awarenessProtocol.encodeAwarenessUpdate(\n this.awarenessUpdates!.protocol,\n changedClients\n )\n )\n const success = await send(\n encoder,\n this.awarenessUpdates!.sendUrl,\n this.fetchClient ?? fetch,\n this.awarenessUpdates!.sendErrorRetryHandler\n )\n if (!success) {\n this.disconnect()\n }\n }\n } finally {\n this.sendingAwarenessState = false\n }\n }\n\n private awarenessShapeHandler(messages: Message<RowWithAwarenessUpdate>[]) {\n for (const message of messages) {\n if (isChangeMessage(message)) {\n if (message.headers.operation === `delete`) {\n awarenessProtocol.removeAwarenessStates(\n this.awarenessUpdates!.protocol,\n [Number(message.value.client_id)],\n `remote`\n )\n } else {\n const decoder = this.awarenessUpdates!.getUpdateFromRow(message.value)\n awarenessProtocol.applyAwarenessUpdate(\n this.awarenessUpdates!.protocol,\n decoding.readVarUint8Array(decoder),\n this\n )\n }\n }\n }\n }\n}\n\nasync function send(\n encoder: encoding.Encoder,\n endpoint: string | URL,\n fetchClient: typeof fetch,\n retryHandler?: SendErrorRetryHandler\n): Promise<boolean> {\n let response: Response | undefined\n const op = encoding.toUint8Array(encoder)\n\n try {\n response = await fetchClient(endpoint!, {\n method: `PUT`,\n headers: {\n 'Content-Type': `application/octet-stream`,\n },\n body: op as BodyInit,\n })\n\n if (!response.ok) {\n throw new Error(`Server did not return 2xx`)\n }\n\n return true\n } catch (error) {\n const shouldRetry = await (retryHandler?.({\n response,\n error,\n }) ?? false)\n return shouldRetry\n }\n}\n","import { ResumeState, ElectricResumeStateProvider } from './types'\nimport { ObservableV2 } from 'lib0/observable.js'\nimport { ElectricProvider } from './y-electric'\nimport * as buffer from 'lib0/buffer'\n\n/**\n * A ResumeStateProvider implementation using LocalStorage.\n * This is a reference implementation that can be used as a starting point\n * for implementing other ResumeStateProviders.\n */\nexport class LocalStorageResumeStateProvider extends ObservableV2<ElectricResumeStateProvider> {\n private key: string\n private resumeState?: ResumeState\n\n constructor(key: string) {\n super()\n this.key = key\n }\n\n subscribeToResumeState(provider: ElectricProvider): () => void {\n const resumeStateHandler = provider.on(`resumeState`, this.save.bind(this))\n return () => provider.off(`resumeState`, resumeStateHandler)\n }\n\n save(resumeState: ResumeState) {\n const jsonPart = JSON.stringify({\n operations: resumeState.document,\n })\n localStorage.setItem(this.key, jsonPart)\n\n if (resumeState.stableStateVector) {\n const vectorBase64 = buffer.toBase64(resumeState.stableStateVector)\n localStorage.setItem(`${this.key}_vector`, vectorBase64)\n } else {\n // ensure vector is removed\n localStorage.removeItem(`${this.key}_vector`)\n }\n }\n\n load(): ResumeState {\n if (this.resumeState) {\n return this.resumeState\n }\n\n const jsonData = localStorage.getItem(this.key)\n if (!jsonData) {\n this.emit(`synced`, [{}])\n } else {\n this.resumeState = JSON.parse(jsonData)\n\n const vectorData = localStorage.getItem(`${this.key}_vector`)\n if (vectorData) {\n this.resumeState!.stableStateVector = buffer.fromBase64(vectorData)\n }\n\n this.emit(`synced`, [this.resumeState!])\n }\n\n return this.resumeState!\n }\n}\n","import * as decoding from 'lib0/decoding'\n\n/**\n * Convert a hex string from PostgreSQL's bytea format to a Uint8Array\n */\nconst hexStringToUint8Array = (hexString: string) => {\n const cleanHexString = hexString.startsWith(`\\\\x`)\n ? hexString.slice(2)\n : hexString\n return new Uint8Array(\n cleanHexString.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16))\n )\n}\n\n/**\n * Utility to parse hex string bytea data to a decoder for YJS operations\n */\nexport const parseToDecoder = {\n bytea: (hexString: string) => {\n const uint8Array = hexStringToUint8Array(hexString)\n return decoding.createDecoder(uint8Array)\n },\n}\n"],"mappings":"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","names":["encoding","decoding","awarenessProtocol","ObservableV2","env","Y","isChangeMessage","isControlMessage","ShapeStream","ElectricProvider","ObservableV2","doc","documentUpdatesConfig","awarenessUpdatesConfig","resumeState","connect","fetchClient","debounceMs","_a","state","update","client","abortController","operationsStream","ShapeStream","__spreadProps","__spreadValues","operationsShapeUnsubscribe","messages","awarenessShapeUnsubscribe","offset","handle","message","isChangeMessage","decoder","operation","isControlMessage","origin","sending","encoder","send","awarenessUpdate","added","updated","removed","changedClients","endpoint","retryHandler","response","op","error","ObservableV2","buffer","LocalStorageResumeStateProvider","key","provider","resumeStateHandler","resumeState","jsonPart","vectorBase64","jsonData","vectorData","decoding","hexStringToUint8Array","hexString","cleanHexString","byte","parseToDecoder","uint8Array"]}
package/dist/index.d.ts CHANGED
@@ -39,7 +39,6 @@ type YProvider = {
39
39
  * A resume state provider is used to persist the sync state of a document
40
40
  * This is composed of:
41
41
  * - The document shape offset and handle
42
- * - The awareness shape offset and handle (optional)
43
42
  * - The state vector of the document synced to the server (optional)
44
43
  */
45
44
  type ElectricResumeStateProvider = {
@@ -63,6 +62,7 @@ type ElectricResumeStateProvider = {
63
62
  * @param resumeState (optional) The resume state to use for the provider. If no resume state the provider will fetch the entire shape.
64
63
  * @param connect (optional) Whether to automatically connect upon initialization.
65
64
  * @param fetchClient (optional) Custom fetch implementation to use for send requests.
65
+ * @param debounceMs (optional) Debounce window in milliseconds for sending document updates. If 0 or undefined, debouncing is disabled and updates are sent immediately.
66
66
  */
67
67
  type ElectricProviderOptions<RowWithDocumentUpdate extends Row<decoding.Decoder>, RowWithAwarenessUpdate extends Row<decoding.Decoder> = never> = {
68
68
  doc: Y.Doc;
@@ -82,16 +82,13 @@ type ElectricProviderOptions<RowWithDocumentUpdate extends Row<decoding.Decoder>
82
82
  resumeState?: ResumeState;
83
83
  connect?: boolean;
84
84
  fetchClient?: typeof fetch;
85
+ debounceMs?: number;
85
86
  };
86
87
  type ResumeState = {
87
88
  document?: {
88
89
  offset: Offset;
89
90
  handle: string;
90
91
  };
91
- awareness?: {
92
- offset: Offset;
93
- handle: string;
94
- };
95
92
  stableStateVector?: Uint8Array;
96
93
  };
97
94
 
@@ -106,6 +103,8 @@ declare class ElectricProvider<RowWithDocumentUpdate extends Row<decoding.Decode
106
103
  private pendingChanges;
107
104
  private sendingAwarenessState;
108
105
  private pendingAwarenessUpdate;
106
+ private debounceMs;
107
+ private debounceTimer;
109
108
  private documentUpdateHandler;
110
109
  private awarenessUpdateHandler?;
111
110
  private exitHandler;
@@ -131,13 +130,16 @@ declare class ElectricProvider<RowWithDocumentUpdate extends Row<decoding.Decode
131
130
  * @param {ResumeState} [options.resumeState] - Resume state for the provider
132
131
  * @param {boolean} [options.connect=true] - Whether to automatically connect upon initialization
133
132
  * @param {typeof fetch} [options.fetchClient] - Custom fetch implementation to use for HTTP requests
133
+ * @param {number} [options.debounceMs] - Debounce window in milliseconds for sending document updates. If 0 or undefined, debouncing is disabled.
134
134
  */
135
- constructor({ doc, documentUpdates: documentUpdatesConfig, awarenessUpdates: awarenessUpdatesConfig, resumeState, connect, fetchClient, }: ElectricProviderOptions<RowWithDocumentUpdate, RowWithAwarenessUpdate>);
135
+ constructor({ doc, documentUpdates: documentUpdatesConfig, awarenessUpdates: awarenessUpdatesConfig, resumeState, connect, fetchClient, debounceMs, }: ElectricProviderOptions<RowWithDocumentUpdate, RowWithAwarenessUpdate>);
136
136
  get synced(): boolean;
137
137
  set synced(state: boolean);
138
138
  set connected(state: boolean);
139
139
  get connected(): boolean;
140
140
  private batch;
141
+ private clearDebounceTimer;
142
+ private scheduleSendOperations;
141
143
  destroy(): void;
142
144
  disconnect(): void;
143
145
  connect(): void;