@syncular/server 0.30.2 → 0.30.3
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/dist/postgres-storage.js +37 -26
- package/package.json +2 -2
- package/src/postgres-storage.ts +52 -36
package/dist/postgres-storage.js
CHANGED
|
@@ -686,34 +686,41 @@ class PostgresTransaction {
|
|
|
686
686
|
const commitSeq = asNumber(rows[0]?.commit_seq);
|
|
687
687
|
if (commit.changes.length === 0)
|
|
688
688
|
return commitSeq;
|
|
689
|
-
// One
|
|
690
|
-
// entries, and the §5 delete tombstones
|
|
689
|
+
// One writable-CTE chain per chunk of changes: the changes, their
|
|
690
|
+
// inverted scope entries, and the §5 delete tombstones. A statement binds
|
|
691
|
+
// at most a 16-bit count of parameters, so a chunk carries
|
|
692
|
+
// PG_CHANGES_PER_STATEMENT changes; every chunk runs in the
|
|
693
|
+
// commit's transaction under the same commit_seq with its global idx.
|
|
691
694
|
// Serialized scopes bind as text before parsing JSONB; drivers that
|
|
692
695
|
// encode JSONB parameters would otherwise store the string as a scalar.
|
|
693
|
-
// A commit can delete one row twice, so
|
|
694
|
-
// row per key
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
696
|
+
// A commit can delete one row twice, so a chunk's tombstone insert keeps
|
|
697
|
+
// one row per key and a later chunk updates it to the same commit_seq.
|
|
698
|
+
for (let first = 0; first < commit.changes.length; first += PG_CHANGES_PER_STATEMENT) {
|
|
699
|
+
const params = [p, commitSeq];
|
|
700
|
+
const values = commit.changes
|
|
701
|
+
.slice(first, first + PG_CHANGES_PER_STATEMENT)
|
|
702
|
+
.map((change, offset) => {
|
|
703
|
+
const base = params.length;
|
|
704
|
+
params.push(first + offset, change.table, change.rowId, change.op === 'upsert' ? 1 : 2, change.rowVersion ?? null, JSON.stringify(change.scopes), change.payload ?? null);
|
|
705
|
+
return `($1,$2::bigint,$${base + 1}::int,$${base + 2},$${base + 3},$${base + 4}::smallint,$${base + 5}::bigint,$${base + 6}::text::jsonb,$${base + 7}::bytea)`;
|
|
706
|
+
});
|
|
707
|
+
await q.query(`WITH inserted AS (
|
|
708
|
+
INSERT INTO sync_changes(partition, commit_seq, idx, tbl, row_id, op, row_version, scopes, payload)
|
|
709
|
+
VALUES ${values.join(',')}
|
|
710
|
+
RETURNING partition, tbl, row_id, op, commit_seq, scopes
|
|
711
|
+
),
|
|
712
|
+
scoped AS (
|
|
713
|
+
INSERT INTO sync_change_scopes(partition, tbl, var, value, commit_seq)
|
|
714
|
+
SELECT inserted.partition, inserted.tbl, scope.key, scope.value, inserted.commit_seq
|
|
715
|
+
FROM inserted CROSS JOIN LATERAL jsonb_each_text(inserted.scopes) AS scope
|
|
716
|
+
ON CONFLICT DO NOTHING
|
|
717
|
+
)
|
|
718
|
+
INSERT INTO sync_tombstones(partition, tbl, row_id, commit_seq)
|
|
719
|
+
SELECT DISTINCT ON (tbl, row_id) partition, tbl, row_id, commit_seq
|
|
720
|
+
FROM inserted WHERE op = 2
|
|
721
|
+
ON CONFLICT (partition, tbl, row_id)
|
|
722
|
+
DO UPDATE SET commit_seq = excluded.commit_seq`, params);
|
|
723
|
+
}
|
|
717
724
|
return commitSeq;
|
|
718
725
|
}
|
|
719
726
|
async putPushResult(clientId, clientCommitId, result) {
|
|
@@ -776,6 +783,10 @@ class RollbackSignal extends Error {
|
|
|
776
783
|
this.name = 'RollbackSignal';
|
|
777
784
|
}
|
|
778
785
|
}
|
|
786
|
+
/** Changes per sync_changes statement: 2 + 7 × 4,096 = 28,674 bound
|
|
787
|
+
* parameters. The Bind message counts parameters in 16 bits; PostgreSQL reads
|
|
788
|
+
* up to 65,535, and PGlite 0.5 corrupts its state above 32,767. */
|
|
789
|
+
const PG_CHANGES_PER_STATEMENT = 4096;
|
|
779
790
|
export class PostgresServerStorage {
|
|
780
791
|
#exec;
|
|
781
792
|
/** Set by `ensureSchema`: app-table lookup for the relational row store. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/server",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.3",
|
|
4
4
|
"description": "Syncular server: handleSyncRequest + storage/auth interfaces for the sync protocol",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"verify:node": "cd ../.. && bun build ./packages/server/test/sqlite-runtime/verify-node.mjs --target=node --conditions=bun --outfile=./packages/server/.verify-node.built.mjs && node ./packages/server/.verify-node.built.mjs"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@syncular/core": "0.30.
|
|
71
|
+
"@syncular/core": "0.30.3"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@electric-sql/pglite": "^0.5.4"
|
package/src/postgres-storage.ts
CHANGED
|
@@ -1117,45 +1117,56 @@ class PostgresTransaction implements StorageTransaction {
|
|
|
1117
1117
|
);
|
|
1118
1118
|
const commitSeq = asNumber(rows[0]?.commit_seq);
|
|
1119
1119
|
if (commit.changes.length === 0) return commitSeq;
|
|
1120
|
-
// One
|
|
1121
|
-
// entries, and the §5 delete tombstones
|
|
1120
|
+
// One writable-CTE chain per chunk of changes: the changes, their
|
|
1121
|
+
// inverted scope entries, and the §5 delete tombstones. A statement binds
|
|
1122
|
+
// at most a 16-bit count of parameters, so a chunk carries
|
|
1123
|
+
// PG_CHANGES_PER_STATEMENT changes; every chunk runs in the
|
|
1124
|
+
// commit's transaction under the same commit_seq with its global idx.
|
|
1122
1125
|
// Serialized scopes bind as text before parsing JSONB; drivers that
|
|
1123
1126
|
// encode JSONB parameters would otherwise store the string as a scalar.
|
|
1124
|
-
// A commit can delete one row twice, so
|
|
1125
|
-
// row per key
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
change
|
|
1135
|
-
|
|
1136
|
-
|
|
1127
|
+
// A commit can delete one row twice, so a chunk's tombstone insert keeps
|
|
1128
|
+
// one row per key and a later chunk updates it to the same commit_seq.
|
|
1129
|
+
for (
|
|
1130
|
+
let first = 0;
|
|
1131
|
+
first < commit.changes.length;
|
|
1132
|
+
first += PG_CHANGES_PER_STATEMENT
|
|
1133
|
+
) {
|
|
1134
|
+
const params: unknown[] = [p, commitSeq];
|
|
1135
|
+
const values = commit.changes
|
|
1136
|
+
.slice(first, first + PG_CHANGES_PER_STATEMENT)
|
|
1137
|
+
.map((change, offset) => {
|
|
1138
|
+
const base = params.length;
|
|
1139
|
+
params.push(
|
|
1140
|
+
first + offset,
|
|
1141
|
+
change.table,
|
|
1142
|
+
change.rowId,
|
|
1143
|
+
change.op === 'upsert' ? 1 : 2,
|
|
1144
|
+
change.rowVersion ?? null,
|
|
1145
|
+
JSON.stringify(change.scopes),
|
|
1146
|
+
change.payload ?? null,
|
|
1147
|
+
);
|
|
1148
|
+
return `($1,$2::bigint,$${base + 1}::int,$${base + 2},$${base + 3},$${base + 4}::smallint,$${base + 5}::bigint,$${base + 6}::text::jsonb,$${base + 7}::bytea)`;
|
|
1149
|
+
});
|
|
1150
|
+
await q.query(
|
|
1151
|
+
`WITH inserted AS (
|
|
1152
|
+
INSERT INTO sync_changes(partition, commit_seq, idx, tbl, row_id, op, row_version, scopes, payload)
|
|
1153
|
+
VALUES ${values.join(',')}
|
|
1154
|
+
RETURNING partition, tbl, row_id, op, commit_seq, scopes
|
|
1155
|
+
),
|
|
1156
|
+
scoped AS (
|
|
1157
|
+
INSERT INTO sync_change_scopes(partition, tbl, var, value, commit_seq)
|
|
1158
|
+
SELECT inserted.partition, inserted.tbl, scope.key, scope.value, inserted.commit_seq
|
|
1159
|
+
FROM inserted CROSS JOIN LATERAL jsonb_each_text(inserted.scopes) AS scope
|
|
1160
|
+
ON CONFLICT DO NOTHING
|
|
1161
|
+
)
|
|
1162
|
+
INSERT INTO sync_tombstones(partition, tbl, row_id, commit_seq)
|
|
1163
|
+
SELECT DISTINCT ON (tbl, row_id) partition, tbl, row_id, commit_seq
|
|
1164
|
+
FROM inserted WHERE op = 2
|
|
1165
|
+
ON CONFLICT (partition, tbl, row_id)
|
|
1166
|
+
DO UPDATE SET commit_seq = excluded.commit_seq`,
|
|
1167
|
+
params,
|
|
1137
1168
|
);
|
|
1138
|
-
|
|
1139
|
-
});
|
|
1140
|
-
await q.query(
|
|
1141
|
-
`WITH inserted AS (
|
|
1142
|
-
INSERT INTO sync_changes(partition, commit_seq, idx, tbl, row_id, op, row_version, scopes, payload)
|
|
1143
|
-
VALUES ${values.join(',')}
|
|
1144
|
-
RETURNING partition, tbl, row_id, op, commit_seq, scopes
|
|
1145
|
-
),
|
|
1146
|
-
scoped AS (
|
|
1147
|
-
INSERT INTO sync_change_scopes(partition, tbl, var, value, commit_seq)
|
|
1148
|
-
SELECT inserted.partition, inserted.tbl, scope.key, scope.value, inserted.commit_seq
|
|
1149
|
-
FROM inserted CROSS JOIN LATERAL jsonb_each_text(inserted.scopes) AS scope
|
|
1150
|
-
ON CONFLICT DO NOTHING
|
|
1151
|
-
)
|
|
1152
|
-
INSERT INTO sync_tombstones(partition, tbl, row_id, commit_seq)
|
|
1153
|
-
SELECT DISTINCT ON (tbl, row_id) partition, tbl, row_id, commit_seq
|
|
1154
|
-
FROM inserted WHERE op = 2
|
|
1155
|
-
ON CONFLICT (partition, tbl, row_id)
|
|
1156
|
-
DO UPDATE SET commit_seq = excluded.commit_seq`,
|
|
1157
|
-
params,
|
|
1158
|
-
);
|
|
1169
|
+
}
|
|
1159
1170
|
return commitSeq;
|
|
1160
1171
|
}
|
|
1161
1172
|
|
|
@@ -1234,6 +1245,11 @@ class RollbackSignal extends Error {
|
|
|
1234
1245
|
}
|
|
1235
1246
|
}
|
|
1236
1247
|
|
|
1248
|
+
/** Changes per sync_changes statement: 2 + 7 × 4,096 = 28,674 bound
|
|
1249
|
+
* parameters. The Bind message counts parameters in 16 bits; PostgreSQL reads
|
|
1250
|
+
* up to 65,535, and PGlite 0.5 corrupts its state above 32,767. */
|
|
1251
|
+
const PG_CHANGES_PER_STATEMENT = 4096;
|
|
1252
|
+
|
|
1237
1253
|
/** One page read waiting in `PostgresServerStorage#batchedPage`. */
|
|
1238
1254
|
interface PendingPage {
|
|
1239
1255
|
readonly spec: Record<string, unknown>;
|