@syncular/server 0.38.0 → 0.39.1
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/content-encoding.d.ts +24 -23
- package/dist/content-encoding.js +77 -22
- package/dist/d1-storage.js +1 -1
- package/dist/postgres-storage.d.ts +1 -1
- package/dist/postgres-storage.js +207 -64
- package/dist/pull.d.ts +7 -3
- package/dist/pull.js +21 -12
- package/dist/relational-rows.d.ts +16 -10
- package/dist/relational-rows.js +48 -42
- package/dist/sqlite-storage.js +16 -3
- package/package.json +2 -2
- package/src/content-encoding.ts +99 -31
- package/src/d1-storage.ts +1 -1
- package/src/postgres-storage.ts +277 -93
- package/src/pull.ts +31 -18
- package/src/relational-rows.ts +53 -47
- package/src/sqlite-storage.ts +20 -3
package/dist/relational-rows.js
CHANGED
|
@@ -464,24 +464,22 @@ export function upsertValues(table, partition, row, dialect) {
|
|
|
464
464
|
row.columnVersions ?? null,
|
|
465
465
|
];
|
|
466
466
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
return out.join(',');
|
|
474
|
-
}
|
|
475
|
-
/** Upsert statement text (bind with `upsertValues`). */
|
|
476
|
-
export function upsertSql(table, dialect) {
|
|
467
|
+
/**
|
|
468
|
+
* Upsert statement text (bind with `upsertValues`). PostgreSQL takes `rows`
|
|
469
|
+
* rows in one statement, bound as consecutive `upsertValues` lists; their
|
|
470
|
+
* row ids must be distinct.
|
|
471
|
+
*/
|
|
472
|
+
export function upsertSql(table, dialect, rows = 1) {
|
|
477
473
|
const names = tableColumnNames(table);
|
|
478
474
|
const quoted = names.map((name) => quoteIdent(name)).join(', ');
|
|
479
|
-
const values =
|
|
475
|
+
const values = dialect === 'sqlite'
|
|
476
|
+
? `(${new Array(names.length).fill('?').join(',')})`
|
|
477
|
+
: Array.from({ length: rows }, (_, row) => `(${names.map((_, column) => `$${row * names.length + column + 1}`).join(',')})`).join(',');
|
|
480
478
|
const updates = names
|
|
481
479
|
.slice(2) // partition + row id are the conflict key
|
|
482
480
|
.map((name) => `${quoteIdent(name)}=excluded.${quoteIdent(name)}`)
|
|
483
481
|
.join(', ');
|
|
484
|
-
return `INSERT INTO ${quoteIdent(table.name)} (${quoted}) VALUES
|
|
482
|
+
return `INSERT INTO ${quoteIdent(table.name)} (${quoted}) VALUES ${values}
|
|
485
483
|
ON CONFLICT (${quoteIdent(SYNC_PARTITION_COLUMN)}, ${quoteIdent(SYNC_ROW_ID_COLUMN)}) DO UPDATE SET ${updates}`;
|
|
486
484
|
}
|
|
487
485
|
/**
|
|
@@ -611,26 +609,40 @@ export function commitWindowPageSql(valueCount, dialect) {
|
|
|
611
609
|
}
|
|
612
610
|
/**
|
|
613
611
|
* PostgreSQL set-based form of `scanRowPageSql`: one statement answers the
|
|
614
|
-
* page of every subscription scanning
|
|
615
|
-
* of `{i, tbl, var, vals, after, lim}` records; each record runs the
|
|
616
|
-
* index-first candidate subquery through a LATERAL join, so every
|
|
617
|
-
* subscription keeps its own keyset cursor and LIMIT.
|
|
618
|
-
*
|
|
612
|
+
* page of every subscription scanning any table in a pull. `$2` is a JSON
|
|
613
|
+
* array of `{i, tbl, var, vals, after, lim}` records; each record runs the
|
|
614
|
+
* same index-first candidate subquery through a LATERAL join, so every
|
|
615
|
+
* subscription keeps its own keyset cursor and LIMIT. The candidate side is
|
|
616
|
+
* table-agnostic (a spec names its table), so the statement joins the row
|
|
617
|
+
* table once per table present in the batch through a UNION ALL branch that
|
|
618
|
+
* keeps only its own candidates. Rows arrive ordered by (i, row_id).
|
|
619
619
|
*
|
|
620
620
|
* Params: [partition, specs (JSON text)]. The specs bind as text before
|
|
621
621
|
* the JSONB cast, so a driver that JSON-encodes a string bound to a `jsonb`
|
|
622
622
|
* parameter (Bun.sql) cannot turn the array into a JSON string.
|
|
623
623
|
*/
|
|
624
|
-
export function scanRowBatchSql(
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
624
|
+
export function scanRowBatchSql(tables) {
|
|
625
|
+
// One branch per table, each keeping only its own candidates and selecting
|
|
626
|
+
// the same meta columns so the caller decodes one row shape.
|
|
627
|
+
const projection = tables
|
|
628
|
+
.map((table) => {
|
|
629
|
+
const quoted = quoteIdent(table.name);
|
|
630
|
+
return `SELECT c.sub AS sub, c.row_id AS row_id, r.${quoteIdent(SYNC_VERSION_COLUMN)} AS server_version, r.${quoteIdent(SYNC_SCOPES_COLUMN)} AS scopes, r.${quoteIdent(SYNC_PAYLOAD_COLUMN)} AS payload, r.${quoteIdent(SYNC_COLUMN_VERSIONS_COLUMN)} AS column_versions
|
|
631
|
+
FROM cand c
|
|
632
|
+
LEFT JOIN ${quoted} r
|
|
633
|
+
ON r.${quoteIdent(SYNC_PARTITION_COLUMN)}=$1 AND r.${quoteIdent(SYNC_ROW_ID_COLUMN)}=c.row_id
|
|
634
|
+
WHERE c.tbl='${table.name.replaceAll("'", "''")}'`;
|
|
635
|
+
})
|
|
636
|
+
.join('\n UNION ALL ');
|
|
637
|
+
return `WITH cand AS (
|
|
638
|
+
SELECT s.i AS sub, c.row_id AS row_id, s.tbl AS tbl
|
|
639
|
+
FROM jsonb_to_recordset($2::text::jsonb) AS s(i int, tbl text, var text, vals text[], after text, lim int)
|
|
640
|
+
CROSS JOIN LATERAL (SELECT DISTINCT row_id FROM sync_row_scopes
|
|
641
|
+
WHERE partition=$1 AND tbl=s.tbl AND var=s.var AND value=ANY(s.vals)
|
|
642
|
+
AND row_id>s.after
|
|
643
|
+
ORDER BY row_id LIMIT s.lim) c)
|
|
644
|
+
${projection}
|
|
645
|
+
ORDER BY sub, row_id`;
|
|
634
646
|
}
|
|
635
647
|
/**
|
|
636
648
|
* PostgreSQL set-based form of `commitWindowPageSql`: one statement answers
|
|
@@ -863,23 +875,17 @@ export function selectRowsForRewriteSql(table, dialect) {
|
|
|
863
875
|
LIMIT ${p[2]}`;
|
|
864
876
|
}
|
|
865
877
|
/**
|
|
866
|
-
*
|
|
878
|
+
* SQLite/D1: rewrite one row during a migration: refresh the projection columns (when
|
|
867
879
|
* materialized) and the payload. Bind with `rewriteValues`.
|
|
868
880
|
*/
|
|
869
|
-
export function rewriteRowSql(table
|
|
870
|
-
const sets = [
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
sets.push(`${quoteIdent(SYNC_PAYLOAD_COLUMN)}=${dialect === 'sqlite' ? '?' : `$${i}`}`);
|
|
879
|
-
i++;
|
|
880
|
-
const wherePartition = dialect === 'sqlite' ? '?' : `$${i}`;
|
|
881
|
-
const whereRowId = dialect === 'sqlite' ? '?' : `$${i + 1}`;
|
|
882
|
-
return `UPDATE ${quoteIdent(table.name)} SET ${sets.join(', ')} WHERE ${quoteIdent(SYNC_PARTITION_COLUMN)}=${wherePartition} AND ${quoteIdent(SYNC_ROW_ID_COLUMN)}=${whereRowId}`;
|
|
881
|
+
export function rewriteRowSql(table) {
|
|
882
|
+
const sets = [
|
|
883
|
+
...(table.materialize
|
|
884
|
+
? table.columns.map((column) => `${quoteIdent(column.name)}=?`)
|
|
885
|
+
: []),
|
|
886
|
+
`${quoteIdent(SYNC_PAYLOAD_COLUMN)}=?`,
|
|
887
|
+
];
|
|
888
|
+
return `UPDATE ${quoteIdent(table.name)} SET ${sets.join(', ')} WHERE ${quoteIdent(SYNC_PARTITION_COLUMN)}=? AND ${quoteIdent(SYNC_ROW_ID_COLUMN)}=?`;
|
|
883
889
|
}
|
|
884
890
|
/** Bind values for `rewriteRowSql` from the (already migrated) payload. */
|
|
885
891
|
export function rewriteValues(table, partition, rowId, payload, dialect) {
|
package/dist/sqlite-storage.js
CHANGED
|
@@ -364,8 +364,6 @@ END`);
|
|
|
364
364
|
// rewrite stored rows (payload re-encode for layout changes, and/or
|
|
365
365
|
// projection backfill for flipped-on materialization). One
|
|
366
366
|
// transaction: a failed bump leaves no half-state.
|
|
367
|
-
const layouts = parseLayouts(marker?.layouts);
|
|
368
|
-
const retiredTables = retiredTableNames(schema, layouts);
|
|
369
367
|
const existing = new Map();
|
|
370
368
|
const existingIndexes = new Map();
|
|
371
369
|
for (const table of schema.tables.values()) {
|
|
@@ -388,6 +386,21 @@ END`);
|
|
|
388
386
|
await this.#serializeWrite(() => {
|
|
389
387
|
this.db.exec('BEGIN IMMEDIATE');
|
|
390
388
|
try {
|
|
389
|
+
// Another process sharing the database may have bumped between the
|
|
390
|
+
// marker read above and the writer lock; the locked marker decides
|
|
391
|
+
// (SPEC "Schema upgrade writer fence").
|
|
392
|
+
const locked = this.db
|
|
393
|
+
.query('SELECT schema_version, layouts FROM sync_schema_meta WHERE id=1')
|
|
394
|
+
.get();
|
|
395
|
+
if (locked !== null && locked.schema_version > schema.version) {
|
|
396
|
+
throw new Error(`stored schema version ${locked.schema_version} is newer than the configured schema (${schema.version}) — refusing to run an older server against a migrated database`);
|
|
397
|
+
}
|
|
398
|
+
if (locked !== null && locked.schema_version === schema.version) {
|
|
399
|
+
this.db.exec('COMMIT');
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const layouts = parseLayouts(locked?.layouts);
|
|
403
|
+
const retiredTables = retiredTableNames(schema, layouts);
|
|
391
404
|
for (const tableName of retiredTables) {
|
|
392
405
|
this.db
|
|
393
406
|
.query('DELETE FROM sync_row_scopes WHERE tbl=?')
|
|
@@ -521,7 +534,7 @@ END`);
|
|
|
521
534
|
*/
|
|
522
535
|
#rewriteRows(table, oldLayout) {
|
|
523
536
|
const select = selectRowsForRewriteSql(table, 'sqlite');
|
|
524
|
-
const update = this.db.query(rewriteRowSql(table
|
|
537
|
+
const update = this.db.query(rewriteRowSql(table));
|
|
525
538
|
const BATCH = 500;
|
|
526
539
|
let afterPartition = '';
|
|
527
540
|
let afterRowId = '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.1",
|
|
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.
|
|
71
|
+
"@syncular/core": "0.39.1"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@electric-sql/pglite": "^0.5.4"
|
package/src/content-encoding.ts
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* §
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* §1.3 transport compression for every buffered HTTP body the adapters
|
|
3
|
+
* serve — `<mount>/sync`, `<mount>/operations`, blob GETs and segment
|
|
4
|
+
* downloads: negotiate `Content-Encoding` from the request's
|
|
5
|
+
* `Accept-Encoding` (zstd preferred, gzip fallback, identity when the client
|
|
6
|
+
* offers neither), feature-detecting the runtime's codecs, zero
|
|
7
|
+
* dependencies. Bun supplies zstd + gzip natively; every other runtime
|
|
8
|
+
* (workerd, Node, Deno) gets gzip through the standard `CompressionStream`.
|
|
6
9
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* Body bytes are stored and content-addressed UNCOMPRESSED (§5.1); this is a
|
|
11
|
+
* per-response serving concern only. It is never applied to stored objects or
|
|
12
|
+
* the signed-URL path (§5.8: presigned GETs serve the addressed bytes
|
|
13
|
+
* verbatim; edge compression is a deployment concern). Clients MAY gzip a
|
|
14
|
+
* request body (§1.3) — `decodeRequestBody` is the accepting half.
|
|
11
15
|
*
|
|
12
16
|
* Decision data (2026-07-03, 100k-row bench table, Bun 1.3): rows
|
|
13
17
|
* segments 6.2× in 8 ms (zstd) / 7.7× in 42 ms (gzip); sqlite images
|
|
14
18
|
* 3.4× in 14 ms (zstd) / 51 ms (gzip); client decompression ≤ 8 ms.
|
|
15
19
|
*/
|
|
20
|
+
import { syncError } from './errors';
|
|
16
21
|
|
|
17
|
-
export type
|
|
22
|
+
export type ContentEncoding = 'zstd' | 'gzip';
|
|
18
23
|
|
|
19
24
|
interface Codec {
|
|
20
|
-
readonly encoding:
|
|
21
|
-
readonly compress: (bytes: Uint8Array) => Uint8Array
|
|
25
|
+
readonly encoding: ContentEncoding;
|
|
26
|
+
readonly compress: (bytes: Uint8Array) => Uint8Array | Promise<Uint8Array>;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
interface BunCompressors {
|
|
@@ -26,8 +31,22 @@ interface BunCompressors {
|
|
|
26
31
|
readonly gzipSync?: (bytes: Uint8Array) => Uint8Array;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
34
|
+
/** Bodies below this size are served identity (headers outweigh gains). */
|
|
35
|
+
const MIN_COMPRESS_BYTES = 1024;
|
|
36
|
+
|
|
37
|
+
async function gzipWithCompressionStream(
|
|
38
|
+
bytes: Uint8Array,
|
|
39
|
+
): Promise<Uint8Array> {
|
|
40
|
+
const compressed = await new Response(
|
|
41
|
+
new Blob([bytes.slice().buffer as ArrayBuffer])
|
|
42
|
+
.stream()
|
|
43
|
+
.pipeThrough(new CompressionStream('gzip')),
|
|
44
|
+
).arrayBuffer();
|
|
45
|
+
return new Uint8Array(compressed);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Preference order is zstd, gzip (§1.3). A runtime with neither codec
|
|
49
|
+
* serves identity: negotiation, never a hard requirement here. */
|
|
31
50
|
function detectCodecs(): readonly Codec[] {
|
|
32
51
|
const bun = (globalThis as { Bun?: BunCompressors }).Bun;
|
|
33
52
|
const codecs: Codec[] = [];
|
|
@@ -38,17 +57,19 @@ function detectCodecs(): readonly Codec[] {
|
|
|
38
57
|
const gzip = bun?.gzipSync;
|
|
39
58
|
if (typeof gzip === 'function') {
|
|
40
59
|
codecs.push({ encoding: 'gzip', compress: (bytes) => gzip(bytes) });
|
|
60
|
+
} else if (typeof CompressionStream === 'function') {
|
|
61
|
+
codecs.push({ encoding: 'gzip', compress: gzipWithCompressionStream });
|
|
41
62
|
}
|
|
42
63
|
return codecs;
|
|
43
64
|
}
|
|
44
65
|
|
|
45
66
|
const CODECS = detectCodecs();
|
|
46
67
|
|
|
47
|
-
/** Bodies below this size are served identity (headers outweigh gains). */
|
|
48
|
-
const MIN_COMPRESS_BYTES = 1024;
|
|
49
|
-
|
|
50
68
|
/** Does the `Accept-Encoding` header value accept `encoding` (q > 0)? */
|
|
51
|
-
function
|
|
69
|
+
export function acceptsEncoding(
|
|
70
|
+
header: string,
|
|
71
|
+
encoding: ContentEncoding,
|
|
72
|
+
): boolean {
|
|
52
73
|
for (const entry of header.split(',')) {
|
|
53
74
|
const [rawName, ...params] = entry.trim().split(';');
|
|
54
75
|
const name = rawName?.trim().toLowerCase();
|
|
@@ -64,28 +85,41 @@ function accepts(header: string, encoding: SegmentContentEncoding): boolean {
|
|
|
64
85
|
return false;
|
|
65
86
|
}
|
|
66
87
|
|
|
67
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Add `Accept-Encoding` to a response's `Vary` header. Every response whose
|
|
90
|
+
* body was negotiated carries it, so a cache never serves one encoding to a
|
|
91
|
+
* client that asked for another.
|
|
92
|
+
*/
|
|
93
|
+
export function varyOnAcceptEncoding(headers: Headers): void {
|
|
94
|
+
const vary = headers.get('vary');
|
|
95
|
+
if (vary === null) headers.set('vary', 'Accept-Encoding');
|
|
96
|
+
else if (!/\baccept-encoding\b/i.test(vary)) {
|
|
97
|
+
headers.set('vary', `${vary}, Accept-Encoding`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface EncodedBody {
|
|
68
102
|
readonly bytes: Uint8Array;
|
|
69
103
|
/** Absent ⇒ identity (no `Content-Encoding` header). */
|
|
70
|
-
readonly contentEncoding?:
|
|
104
|
+
readonly contentEncoding?: ContentEncoding;
|
|
71
105
|
}
|
|
72
106
|
|
|
73
107
|
/**
|
|
74
|
-
* Compress a buffered
|
|
108
|
+
* Compress a buffered response body per §1.3. Returns the original bytes
|
|
75
109
|
* (identity) when the client offered no supported encoding, the body is
|
|
76
110
|
* tiny, or the runtime has no codec.
|
|
77
111
|
*/
|
|
78
|
-
export function
|
|
112
|
+
export async function encodeBody(
|
|
79
113
|
bytes: Uint8Array,
|
|
80
114
|
acceptEncoding: string | undefined,
|
|
81
|
-
):
|
|
115
|
+
): Promise<EncodedBody> {
|
|
82
116
|
if (acceptEncoding === undefined || bytes.length < MIN_COMPRESS_BYTES) {
|
|
83
117
|
return { bytes };
|
|
84
118
|
}
|
|
85
119
|
for (const codec of CODECS) {
|
|
86
|
-
if (
|
|
120
|
+
if (acceptsEncoding(acceptEncoding, codec.encoding)) {
|
|
87
121
|
return {
|
|
88
|
-
bytes: codec.compress(bytes),
|
|
122
|
+
bytes: await codec.compress(bytes),
|
|
89
123
|
contentEncoding: codec.encoding,
|
|
90
124
|
};
|
|
91
125
|
}
|
|
@@ -100,24 +134,24 @@ export function encodeSegmentBody(
|
|
|
100
134
|
*/
|
|
101
135
|
export const SEGMENT_STREAM_THRESHOLD_BYTES = 16 * 1024 * 1024;
|
|
102
136
|
|
|
103
|
-
export interface
|
|
137
|
+
export interface EncodedStreamBody {
|
|
104
138
|
readonly body: ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
105
139
|
/** Absent ⇒ identity (no `Content-Encoding` header). */
|
|
106
140
|
readonly contentEncoding?: 'gzip';
|
|
107
141
|
}
|
|
108
142
|
|
|
109
143
|
/**
|
|
110
|
-
* Compress a streamed
|
|
111
|
-
*
|
|
112
|
-
*
|
|
144
|
+
* Compress a streamed body per §1.3 with the runtime's `CompressionStream`:
|
|
145
|
+
* gzip when the client accepts it, identity otherwise (zstd has no standard
|
|
146
|
+
* streaming codec).
|
|
113
147
|
*/
|
|
114
|
-
export function
|
|
148
|
+
export function encodeStream(
|
|
115
149
|
body: ReadableStream<Uint8Array<ArrayBuffer>>,
|
|
116
150
|
acceptEncoding: string | undefined,
|
|
117
|
-
):
|
|
151
|
+
): EncodedStreamBody {
|
|
118
152
|
if (
|
|
119
153
|
acceptEncoding === undefined ||
|
|
120
|
-
!
|
|
154
|
+
!acceptsEncoding(acceptEncoding, 'gzip') ||
|
|
121
155
|
typeof CompressionStream !== 'function'
|
|
122
156
|
) {
|
|
123
157
|
return { body };
|
|
@@ -127,3 +161,37 @@ export function encodeSegmentStream(
|
|
|
127
161
|
contentEncoding: 'gzip',
|
|
128
162
|
};
|
|
129
163
|
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The bytes of a request body, decompressed when the client sent
|
|
167
|
+
* `Content-Encoding: gzip` (§1.3: clients MAY compress a request body and
|
|
168
|
+
* servers MUST accept it). Returns `undefined` for any other declared
|
|
169
|
+
* encoding — the adapter answers 415 rather than parse bytes it cannot read.
|
|
170
|
+
* A gzip body that does not decompress is a `sync.invalid_request`.
|
|
171
|
+
*/
|
|
172
|
+
export async function decodeRequestBody(
|
|
173
|
+
request: Request,
|
|
174
|
+
): Promise<Uint8Array | undefined> {
|
|
175
|
+
const encoding = request.headers
|
|
176
|
+
.get('content-encoding')
|
|
177
|
+
?.trim()
|
|
178
|
+
.toLowerCase();
|
|
179
|
+
if (encoding === undefined || encoding === '' || encoding === 'identity') {
|
|
180
|
+
return new Uint8Array(await request.arrayBuffer());
|
|
181
|
+
}
|
|
182
|
+
if (encoding !== 'gzip' || typeof DecompressionStream !== 'function') {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
if (request.body === null) {
|
|
186
|
+
throw syncError('sync.invalid_request', 'malformed gzip request body');
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
return new Uint8Array(
|
|
190
|
+
await new Response(
|
|
191
|
+
request.body.pipeThrough(new DecompressionStream('gzip')),
|
|
192
|
+
).arrayBuffer(),
|
|
193
|
+
);
|
|
194
|
+
} catch {
|
|
195
|
+
throw syncError('sync.invalid_request', 'malformed gzip request body');
|
|
196
|
+
}
|
|
197
|
+
}
|
package/src/d1-storage.ts
CHANGED
|
@@ -1425,7 +1425,7 @@ export class D1ServerStorage implements ServerStorage {
|
|
|
1425
1425
|
? migratePayload(plan.oldLayout, table, bytes)
|
|
1426
1426
|
: bytes;
|
|
1427
1427
|
return this.#db
|
|
1428
|
-
.prepare(rewriteRowSql(table
|
|
1428
|
+
.prepare(rewriteRowSql(table))
|
|
1429
1429
|
.bind(
|
|
1430
1430
|
...rewriteValues(
|
|
1431
1431
|
table,
|