@jcbuisson/express-x 3.1.12 → 3.1.13

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",
3
- "version": "3.1.12",
3
+ "version": "3.1.13",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "src/server.mjs",
@@ -1373,6 +1373,68 @@ describe('Full offline-first client ↔ server protocol', () => {
1373
1373
  }
1374
1374
  })
1375
1375
 
1376
+ test('scoped reconnect sync preserves dirty records that moved out of scope', async () => {
1377
+ const modelName = `model${++dbCounter}`
1378
+ const { pglite, db, metaTable, modelTable } = await createTestDb(modelName)
1379
+
1380
+ await db.insert(modelTable).values({ uid: 'r1', label: 'open' })
1381
+ await db.insert(metaTable).values({ uid: 'r1', created_at: T0 })
1382
+
1383
+ const serverApp = expressX({})
1384
+ serverApp.configure(drizzleOfflinePlugin, db, metaTable, [modelTable])
1385
+ await new Promise(resolve => serverApp.httpServer.listen(0, resolve))
1386
+ const port = serverApp.httpServer.address().port
1387
+
1388
+ const socket = ioc(`http://localhost:${port}`, {
1389
+ transports: ['websocket'],
1390
+ autoConnect: false,
1391
+ })
1392
+ const clientApp = createClient(socket, { debug: false })
1393
+ offlinePlugin(clientApp)
1394
+ const model = clientApp.createOfflineModel(modelName, ['label'])
1395
+
1396
+ try {
1397
+ socket.connect()
1398
+ await new Promise((resolve, reject) => {
1399
+ socket.on('connect', resolve)
1400
+ socket.on('connect_error', reject)
1401
+ })
1402
+
1403
+ await model.addSynchroWhere({ label: 'open' })
1404
+ await model.synchronizeAll()
1405
+ assert.equal((await model.db.values.get('r1')).label, 'open')
1406
+
1407
+ socket.disconnect()
1408
+ for (let i = 0; i < 50 && clientApp.isConnected; i++) {
1409
+ await new Promise(resolve => setTimeout(resolve, 10))
1410
+ }
1411
+ assert.equal(clientApp.isConnected, false)
1412
+
1413
+ await model.update('r1', { label: 'closed' })
1414
+ assert.equal((await model.db.values.get('r1')).label, 'closed')
1415
+
1416
+ socket.connect()
1417
+ await new Promise((resolve, reject) => {
1418
+ socket.on('connect', resolve)
1419
+ socket.on('connect_error', reject)
1420
+ })
1421
+
1422
+ let serverRows = []
1423
+ for (let i = 0; i < 50; i++) {
1424
+ serverRows = await db.select().from(modelTable).where(eq(modelTable.uid, 'r1'))
1425
+ if (serverRows[0]?.label === 'closed') break
1426
+ await new Promise(resolve => setTimeout(resolve, 10))
1427
+ }
1428
+
1429
+ assert.equal(serverRows[0]?.label, 'closed', 'dirty out-of-scope client update must be pushed to the server')
1430
+ assert.equal((await model.db.values.get('r1')).label, 'closed', 'client value must not be overwritten by stale scoped server data')
1431
+ } finally {
1432
+ socket.disconnect()
1433
+ await new Promise(resolve => serverApp.io.close(resolve))
1434
+ pglite.close()
1435
+ }
1436
+ })
1437
+
1376
1438
  test('update() rollback clears stale updated_at from metadata', async () => {
1377
1439
  // Optimistic update sets updated_at = now in metadata before the server responds.
1378
1440
  // On rejection the rollback does db.metadata.update(uid, previousMetadata), but