@jcbuisson/express-x-plugins 5.0.3 → 5.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcbuisson/express-x-plugins",
3
- "version": "5.0.3",
3
+ "version": "5.0.4",
4
4
  "description": "Plugins for express-x",
5
5
  "type": "module",
6
6
  "main": "./src/electric-server-plugin.mjs",
@@ -158,20 +158,23 @@ export function createOfflineElectricModel({ db, service, modelName, url, primar
158
158
 
159
159
  async function reconcileSnapshot(rows) {
160
160
  await retryLeader(() => db.transaction(async tx => {
161
- for (const row of rows) {
162
- const id = String(row[primaryKey])
163
- if (row.version != null) {
164
- await tx.query(`DELETE FROM electric_mutation_queue
165
- WHERE model_name = $1 AND row_id = $2 AND acknowledged_version <= $3::numeric`,
166
- [modelName, id, String(row.version)])
167
- }
168
- if (row.deleted) continue
169
- await tx.query(`INSERT INTO electric_local_rows (model_name, row_id, data)
170
- SELECT $1, $2, $3::jsonb WHERE NOT EXISTS
171
- (SELECT 1 FROM electric_mutation_queue WHERE model_name = $1 AND row_id = $2)
172
- ON CONFLICT (model_name, row_id) DO UPDATE SET data = EXCLUDED.data`,
173
- [modelName, id, json(row)])
174
- }
161
+ // Send the snapshot once instead of making two worker round trips per row.
162
+ const incoming = json(rows)
163
+ await tx.query(`DELETE FROM electric_mutation_queue AS pending
164
+ USING jsonb_array_elements($2::jsonb) AS incoming(data)
165
+ WHERE pending.model_name = $1
166
+ AND pending.row_id = incoming.data->>$3
167
+ AND pending.acknowledged_version <= (incoming.data->>'version')::numeric`,
168
+ [modelName, incoming, primaryKey])
169
+ await tx.query(`INSERT INTO electric_local_rows (model_name, row_id, data)
170
+ SELECT $1, incoming.data->>$3, incoming.data
171
+ FROM jsonb_array_elements($2::jsonb) AS incoming(data)
172
+ WHERE NOT COALESCE((incoming.data->>'deleted')::boolean, false)
173
+ AND NOT EXISTS (SELECT 1 FROM electric_mutation_queue AS pending
174
+ WHERE pending.model_name = $1 AND pending.row_id = incoming.data->>$3)
175
+ ON CONFLICT (model_name, row_id) DO UPDATE SET data = EXCLUDED.data
176
+ WHERE electric_local_rows.data IS DISTINCT FROM EXCLUDED.data`,
177
+ [modelName, incoming, primaryKey])
175
178
  const activeIds = rows.filter(row => !row.deleted).map(row => String(row[primaryKey]))
176
179
  await tx.query(`DELETE FROM electric_local_rows AS local
177
180
  WHERE local.model_name = $1 AND NOT (local.row_id = ANY($2::text[]))
@@ -66,3 +66,43 @@ test('offline writes stay local until the acknowledged Electric version arrives'
66
66
  await db.close()
67
67
  }
68
68
  })
69
+
70
+ test('large snapshots use three queries and preserve bigint versions and tombstones', async () => {
71
+ const db = new PGlite()
72
+ const { createOfflineElectricModel } = await import('../src/electric-offline-model.mjs')
73
+ let queries = 0
74
+ const countedDb = {
75
+ exec: (...args) => db.exec(...args),
76
+ query: (...args) => db.query(...args),
77
+ transaction: callback => db.transaction(tx => callback({
78
+ query: (...args) => { queries++; return tx.query(...args) },
79
+ })),
80
+ }
81
+ const model = createOfflineElectricModel({
82
+ db: countedDb, service: {}, modelName: 'range', primaryKey: 'uid',
83
+ ShapeStreamClass: Stream, ShapeClass: Shape, ownsSync: true,
84
+ network: { onLine: true }, retryMs: 60_000,
85
+ })
86
+ try {
87
+ await model.prepare()
88
+ model.start()
89
+ await model.waitForIdle()
90
+ const rows = Array.from({ length: 100 }, () => ({
91
+ uid: crypto.randomUUID(), label: 'shared', version: '9007199254740993', deleted: false,
92
+ }))
93
+ Shape.current.emit({ rows })
94
+ await model.waitForIdle()
95
+ assert.equal(queries, 3)
96
+ assert.equal((await model.findMany()).length, 100)
97
+ assert.equal((await model.findMany())[0].version, '9007199254740993')
98
+ queries = 0
99
+ Shape.current.emit({ rows: [{ ...rows[0], deleted: true }, { ...rows[1], label: 'changed' }] })
100
+ await model.waitForIdle()
101
+ assert.equal(queries, 3)
102
+ assert.deepEqual(await model.findMany(), [{ ...rows[1], label: 'changed' }])
103
+ } finally {
104
+ model.stop()
105
+ await model.waitForIdle()
106
+ await db.close()
107
+ }
108
+ })