@syncular/client 0.0.6-126 → 0.0.6-136
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/blobs/index.d.ts +0 -1
- package/dist/blobs/index.d.ts.map +1 -1
- package/dist/blobs/index.js +0 -1
- package/dist/blobs/index.js.map +1 -1
- package/dist/client.d.ts +21 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +12 -0
- package/dist/client.js.map +1 -1
- package/dist/create-client.d.ts +17 -0
- package/dist/create-client.d.ts.map +1 -1
- package/dist/create-client.js +3 -0
- package/dist/create-client.js.map +1 -1
- package/dist/engine/SyncEngine.d.ts +11 -0
- package/dist/engine/SyncEngine.d.ts.map +1 -1
- package/dist/engine/SyncEngine.js +181 -27
- package/dist/engine/SyncEngine.js.map +1 -1
- package/dist/engine/types.d.ts +17 -0
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/migrate.d.ts +1 -1
- package/dist/migrate.d.ts.map +1 -1
- package/dist/migrate.js +0 -126
- package/dist/migrate.js.map +1 -1
- package/dist/mutations.d.ts.map +1 -1
- package/dist/mutations.js +9 -1
- package/dist/mutations.js.map +1 -1
- package/dist/query/fingerprint.d.ts +1 -1
- package/dist/query/fingerprint.d.ts.map +1 -1
- package/dist/query/fingerprint.js +29 -6
- package/dist/query/fingerprint.js.map +1 -1
- package/dist/sync-loop.d.ts.map +1 -1
- package/dist/sync-loop.js +29 -19
- package/dist/sync-loop.js.map +1 -1
- package/package.json +3 -3
- package/src/blobs/index.ts +0 -1
- package/src/client.ts +37 -0
- package/src/create-client.ts +21 -0
- package/src/engine/SyncEngine.test.ts +257 -0
- package/src/engine/SyncEngine.ts +214 -27
- package/src/engine/types.ts +17 -0
- package/src/migrate.ts +1 -190
- package/src/mutations.ts +9 -1
- package/src/query/fingerprint.test.ts +73 -0
- package/src/query/fingerprint.ts +33 -6
- package/src/sync-loop.ts +29 -19
- package/dist/blobs/manager.d.ts +0 -345
- package/dist/blobs/manager.d.ts.map +0 -1
- package/dist/blobs/manager.js +0 -749
- package/dist/blobs/manager.js.map +0 -1
- package/src/blobs/manager.ts +0 -1027
package/src/engine/types.ts
CHANGED
|
@@ -262,6 +262,23 @@ export interface SyncEngineConfig<DB extends SyncClientDb = SyncClientDb> {
|
|
|
262
262
|
onConflict?: (conflict: ConflictInfo) => void;
|
|
263
263
|
/** Data change callback */
|
|
264
264
|
onDataChange?: (scopes: string[]) => void;
|
|
265
|
+
/**
|
|
266
|
+
* Debounce window for coalescing `data:change` emissions.
|
|
267
|
+
* - `0` (default): emit immediately
|
|
268
|
+
* - `>0`: merge scopes and emit once per window
|
|
269
|
+
*/
|
|
270
|
+
dataChangeDebounceMs?: number;
|
|
271
|
+
/**
|
|
272
|
+
* Override debounce window while `isSyncing === true`.
|
|
273
|
+
* If omitted, `dataChangeDebounceMs` is used.
|
|
274
|
+
*/
|
|
275
|
+
dataChangeDebounceMsWhenSyncing?: number;
|
|
276
|
+
/**
|
|
277
|
+
* Override debounce window while `connectionState === "reconnecting"`.
|
|
278
|
+
* If omitted, `dataChangeDebounceMsWhenSyncing` (if syncing) or
|
|
279
|
+
* `dataChangeDebounceMs` is used.
|
|
280
|
+
*/
|
|
281
|
+
dataChangeDebounceMsWhenReconnecting?: number;
|
|
265
282
|
/** Optional client plugins (e.g. encryption) */
|
|
266
283
|
plugins?: SyncClientPlugin[];
|
|
267
284
|
/** Custom SHA-256 hash function (for platforms without crypto.subtle, e.g. React Native) */
|
package/src/migrate.ts
CHANGED
|
@@ -2,195 +2,9 @@
|
|
|
2
2
|
* @syncular/client - Sync migrations (SQLite reference)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import type { Kysely } from 'kysely';
|
|
6
6
|
import type { SyncClientDb } from './schema';
|
|
7
7
|
|
|
8
|
-
type SyncInternalTable =
|
|
9
|
-
| 'sync_subscription_state'
|
|
10
|
-
| 'sync_outbox_commits'
|
|
11
|
-
| 'sync_conflicts';
|
|
12
|
-
|
|
13
|
-
function toErrorMessage(error: unknown): string {
|
|
14
|
-
return error instanceof Error ? error.message : String(error);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function isMissingTableError(message: string): boolean {
|
|
18
|
-
const normalized = message.toLowerCase();
|
|
19
|
-
return (
|
|
20
|
-
normalized.includes('no such table') ||
|
|
21
|
-
(normalized.includes('relation') && normalized.includes('does not exist'))
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function isMissingColumnError(message: string): boolean {
|
|
26
|
-
const normalized = message.toLowerCase();
|
|
27
|
-
return (
|
|
28
|
-
normalized.includes('no such column') ||
|
|
29
|
-
(normalized.includes('column') && normalized.includes('does not exist'))
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function isDuplicateColumnError(message: string): boolean {
|
|
34
|
-
const normalized = message.toLowerCase();
|
|
35
|
-
return (
|
|
36
|
-
normalized.includes('duplicate column name') ||
|
|
37
|
-
(normalized.includes('column') && normalized.includes('already exists'))
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function getColumnNames<DB extends SyncClientDb>(
|
|
42
|
-
db: Kysely<DB>,
|
|
43
|
-
tableName: SyncInternalTable
|
|
44
|
-
): Promise<Set<string> | null> {
|
|
45
|
-
try {
|
|
46
|
-
const sqlite = await sql<{ name: string }>`
|
|
47
|
-
select name from pragma_table_info(${sql.val(tableName)})
|
|
48
|
-
`.execute(db);
|
|
49
|
-
return new Set(sqlite.rows.map((row) => String(row.name)));
|
|
50
|
-
} catch {
|
|
51
|
-
// Not SQLite or pragma unavailable.
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
const postgres = await sql<{ name: string }>`
|
|
56
|
-
select column_name as name
|
|
57
|
-
from information_schema.columns
|
|
58
|
-
where table_name = ${sql.val(tableName)}
|
|
59
|
-
`.execute(db);
|
|
60
|
-
return new Set(postgres.rows.map((row) => String(row.name)));
|
|
61
|
-
} catch {
|
|
62
|
-
// Introspection unavailable; caller falls back to probing.
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function hasColumn<DB extends SyncClientDb>(
|
|
69
|
-
db: Kysely<DB>,
|
|
70
|
-
tableName: SyncInternalTable,
|
|
71
|
-
columnName: string
|
|
72
|
-
): Promise<boolean> {
|
|
73
|
-
const columns = await getColumnNames(db, tableName);
|
|
74
|
-
if (columns) {
|
|
75
|
-
return columns.has(columnName);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
await sql`select ${sql.ref(columnName)} from ${sql.table(tableName)} limit 1`.execute(
|
|
80
|
-
db
|
|
81
|
-
);
|
|
82
|
-
return true;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
const message = toErrorMessage(error);
|
|
85
|
-
if (isMissingTableError(message) || isMissingColumnError(message)) {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function addColumnIfMissing<DB extends SyncClientDb>(
|
|
93
|
-
db: Kysely<DB>,
|
|
94
|
-
tableName: SyncInternalTable,
|
|
95
|
-
columnName: string,
|
|
96
|
-
addColumn: () => Promise<void>
|
|
97
|
-
): Promise<void> {
|
|
98
|
-
if (await hasColumn(db, tableName, columnName)) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
try {
|
|
102
|
-
await addColumn();
|
|
103
|
-
} catch (error) {
|
|
104
|
-
const message = toErrorMessage(error);
|
|
105
|
-
if (isDuplicateColumnError(message)) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function ensureClientSyncSchemaCompat<DB extends SyncClientDb>(
|
|
113
|
-
db: Kysely<DB>
|
|
114
|
-
): Promise<void> {
|
|
115
|
-
const hasTableColumn = await hasColumn(
|
|
116
|
-
db,
|
|
117
|
-
'sync_subscription_state',
|
|
118
|
-
'table'
|
|
119
|
-
);
|
|
120
|
-
if (
|
|
121
|
-
!hasTableColumn &&
|
|
122
|
-
(await hasColumn(db, 'sync_subscription_state', 'shape'))
|
|
123
|
-
) {
|
|
124
|
-
try {
|
|
125
|
-
await sql`alter table ${sql.table('sync_subscription_state')} rename column ${sql.ref('shape')} to ${sql.ref('table')}`.execute(
|
|
126
|
-
db
|
|
127
|
-
);
|
|
128
|
-
} catch {
|
|
129
|
-
await addColumnIfMissing(
|
|
130
|
-
db,
|
|
131
|
-
'sync_subscription_state',
|
|
132
|
-
'table',
|
|
133
|
-
async () => {
|
|
134
|
-
await db.schema
|
|
135
|
-
.alterTable('sync_subscription_state')
|
|
136
|
-
.addColumn('table', 'text', (col) => col.notNull().defaultTo(''))
|
|
137
|
-
.execute();
|
|
138
|
-
}
|
|
139
|
-
);
|
|
140
|
-
await sql`update ${sql.table('sync_subscription_state')}
|
|
141
|
-
set ${sql.ref('table')} = ${sql.ref('shape')}
|
|
142
|
-
where ${sql.ref('table')} = ${sql.val('')}`.execute(db);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
await addColumnIfMissing(
|
|
147
|
-
db,
|
|
148
|
-
'sync_subscription_state',
|
|
149
|
-
'bootstrap_state_json',
|
|
150
|
-
async () => {
|
|
151
|
-
await db.schema
|
|
152
|
-
.alterTable('sync_subscription_state')
|
|
153
|
-
.addColumn('bootstrap_state_json', 'text')
|
|
154
|
-
.execute();
|
|
155
|
-
}
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
await addColumnIfMissing(
|
|
159
|
-
db,
|
|
160
|
-
'sync_outbox_commits',
|
|
161
|
-
'schema_version',
|
|
162
|
-
async () => {
|
|
163
|
-
await db.schema
|
|
164
|
-
.alterTable('sync_outbox_commits')
|
|
165
|
-
.addColumn('schema_version', 'integer', (col) =>
|
|
166
|
-
col.notNull().defaultTo(1)
|
|
167
|
-
)
|
|
168
|
-
.execute();
|
|
169
|
-
}
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
await addColumnIfMissing(db, 'sync_conflicts', 'resolved_at', async () => {
|
|
173
|
-
await db.schema
|
|
174
|
-
.alterTable('sync_conflicts')
|
|
175
|
-
.addColumn('resolved_at', 'bigint')
|
|
176
|
-
.execute();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
await addColumnIfMissing(db, 'sync_conflicts', 'resolution', async () => {
|
|
180
|
-
await db.schema
|
|
181
|
-
.alterTable('sync_conflicts')
|
|
182
|
-
.addColumn('resolution', 'text')
|
|
183
|
-
.execute();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
await db.schema
|
|
187
|
-
.createIndex('idx_sync_outbox_commits_status_updated_at')
|
|
188
|
-
.ifNotExists()
|
|
189
|
-
.on('sync_outbox_commits')
|
|
190
|
-
.columns(['status', 'updated_at', 'created_at'])
|
|
191
|
-
.execute();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
8
|
/**
|
|
195
9
|
* Ensures the client sync schema exists in the database.
|
|
196
10
|
* Safe to call multiple times (idempotent).
|
|
@@ -249,9 +63,6 @@ export async function ensureClientSyncSchema<DB extends SyncClientDb>(
|
|
|
249
63
|
.addColumn('resolution', 'text')
|
|
250
64
|
.execute();
|
|
251
65
|
|
|
252
|
-
// Apply framework-managed compatibility upgrades for legacy sync tables.
|
|
253
|
-
await ensureClientSyncSchemaCompat(db);
|
|
254
|
-
|
|
255
66
|
await db.schema
|
|
256
67
|
.createIndex('idx_sync_subscription_state_state_sub')
|
|
257
68
|
.ifNotExists()
|
package/src/mutations.ts
CHANGED
|
@@ -168,7 +168,7 @@ function coerceBaseVersion(value: unknown): number | null {
|
|
|
168
168
|
if (value === null || value === undefined) return null;
|
|
169
169
|
const n = typeof value === 'number' ? value : Number(value);
|
|
170
170
|
if (!Number.isFinite(n)) return null;
|
|
171
|
-
if (n
|
|
171
|
+
if (n < 0) return null;
|
|
172
172
|
return n;
|
|
173
173
|
}
|
|
174
174
|
|
|
@@ -498,6 +498,9 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
498
498
|
},
|
|
499
499
|
|
|
500
500
|
async insertMany(rows) {
|
|
501
|
+
if (rows.length === 0) {
|
|
502
|
+
throw new Error('insertMany requires at least one row');
|
|
503
|
+
}
|
|
501
504
|
const ids: string[] = [];
|
|
502
505
|
const toInsert: Record<string, unknown>[] = [];
|
|
503
506
|
|
|
@@ -672,6 +675,7 @@ export function createOutboxCommit<DB extends SyncClientDb>(
|
|
|
672
675
|
get(_target, prop) {
|
|
673
676
|
if (prop === 'then') return undefined;
|
|
674
677
|
if (typeof prop !== 'string') return undefined;
|
|
678
|
+
validateTableName(prop);
|
|
675
679
|
return makeTxTable(prop);
|
|
676
680
|
},
|
|
677
681
|
}
|
|
@@ -777,6 +781,9 @@ export function createPushCommit<DB = AnyDb>(
|
|
|
777
781
|
},
|
|
778
782
|
|
|
779
783
|
async insertMany(rows) {
|
|
784
|
+
if (rows.length === 0) {
|
|
785
|
+
throw new Error('insertMany requires at least one row');
|
|
786
|
+
}
|
|
780
787
|
const ids: string[] = [];
|
|
781
788
|
const toUpsert: Record<string, unknown>[] = [];
|
|
782
789
|
|
|
@@ -885,6 +892,7 @@ export function createPushCommit<DB = AnyDb>(
|
|
|
885
892
|
get(_target, prop) {
|
|
886
893
|
if (prop === 'then') return undefined;
|
|
887
894
|
if (typeof prop !== 'string') return undefined;
|
|
895
|
+
validateTableName(prop);
|
|
888
896
|
return makeTxTable(prop);
|
|
889
897
|
},
|
|
890
898
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { computeRowFingerprint } from './fingerprint';
|
|
3
|
+
|
|
4
|
+
function createTimestampSource(values: Record<string, number>): {
|
|
5
|
+
getMutationTimestamp: (table: string, rowId: string) => number;
|
|
6
|
+
} {
|
|
7
|
+
return {
|
|
8
|
+
getMutationTimestamp(table: string, rowId: string): number {
|
|
9
|
+
return values[`${table}:${rowId}`] ?? 0;
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('computeRowFingerprint', () => {
|
|
15
|
+
it('is deterministic for the same input rows', () => {
|
|
16
|
+
const source = createTimestampSource({
|
|
17
|
+
'tasks:a': 1,
|
|
18
|
+
'tasks:b': 2,
|
|
19
|
+
});
|
|
20
|
+
const rows = [{ id: 'a' }, { id: 'b' }];
|
|
21
|
+
|
|
22
|
+
const first = computeRowFingerprint(rows, 'tasks', source, 'id');
|
|
23
|
+
const second = computeRowFingerprint(rows, 'tasks', source, 'id');
|
|
24
|
+
|
|
25
|
+
expect(first).toBe(second);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('changes when row order changes', () => {
|
|
29
|
+
const source = createTimestampSource({
|
|
30
|
+
'tasks:a': 1,
|
|
31
|
+
'tasks:b': 2,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const first = computeRowFingerprint(
|
|
35
|
+
[{ id: 'a' }, { id: 'b' }],
|
|
36
|
+
'tasks',
|
|
37
|
+
source,
|
|
38
|
+
'id'
|
|
39
|
+
);
|
|
40
|
+
const second = computeRowFingerprint(
|
|
41
|
+
[{ id: 'b' }, { id: 'a' }],
|
|
42
|
+
'tasks',
|
|
43
|
+
source,
|
|
44
|
+
'id'
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(first).not.toBe(second);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('changes when mutation timestamps change', () => {
|
|
51
|
+
const rows = [{ id: 'a' }];
|
|
52
|
+
const first = computeRowFingerprint(
|
|
53
|
+
rows,
|
|
54
|
+
'tasks',
|
|
55
|
+
createTimestampSource({ 'tasks:a': 1 }),
|
|
56
|
+
'id'
|
|
57
|
+
);
|
|
58
|
+
const second = computeRowFingerprint(
|
|
59
|
+
rows,
|
|
60
|
+
'tasks',
|
|
61
|
+
createTimestampSource({ 'tasks:a': 2 }),
|
|
62
|
+
'id'
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(first).not.toBe(second);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns compact hash format', () => {
|
|
69
|
+
const source = createTimestampSource({});
|
|
70
|
+
const fingerprint = computeRowFingerprint([], 'tasks', source, 'id');
|
|
71
|
+
expect(fingerprint).toMatch(/^tasks:0:[0-9a-f]{8}$/);
|
|
72
|
+
});
|
|
73
|
+
});
|
package/src/query/fingerprint.ts
CHANGED
|
@@ -9,6 +9,32 @@ export interface MutationTimestampSource {
|
|
|
9
9
|
getMutationTimestamp(table: string, rowId: string): number;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const FNV_OFFSET_BASIS = 0x811c9dc5;
|
|
13
|
+
const FNV_PRIME = 0x01000193;
|
|
14
|
+
|
|
15
|
+
function hashMix(hash: number, value: number): number {
|
|
16
|
+
return Math.imul(hash ^ value, FNV_PRIME) >>> 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hashString(hash: number, value: string): number {
|
|
20
|
+
let next = hash;
|
|
21
|
+
for (let i = 0; i < value.length; i++) {
|
|
22
|
+
next = hashMix(next, value.charCodeAt(i));
|
|
23
|
+
}
|
|
24
|
+
return next;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hashTimestamp(hash: number, value: number): number {
|
|
28
|
+
if (!Number.isFinite(value)) {
|
|
29
|
+
return hashMix(hash, 0);
|
|
30
|
+
}
|
|
31
|
+
// Keep three decimal places to preserve sub-millisecond precision.
|
|
32
|
+
const scaled = Math.round(value * 1000);
|
|
33
|
+
const lowBits = scaled >>> 0;
|
|
34
|
+
const highBits = Math.floor(scaled / 0x1_0000_0000) >>> 0;
|
|
35
|
+
return hashMix(hashMix(hash, lowBits), highBits);
|
|
36
|
+
}
|
|
37
|
+
|
|
12
38
|
/**
|
|
13
39
|
* Compute a fingerprint for query results based on length + ids + mutation timestamps.
|
|
14
40
|
* Much faster than deep equality for large datasets.
|
|
@@ -69,7 +95,7 @@ export function canFingerprint<T>(rows: T[], keyField = 'id'): boolean {
|
|
|
69
95
|
|
|
70
96
|
/**
|
|
71
97
|
* Compute row-level fingerprint from query results.
|
|
72
|
-
* Format: `table:count:
|
|
98
|
+
* Format: `table:count:hash`
|
|
73
99
|
*/
|
|
74
100
|
export function computeRowFingerprint(
|
|
75
101
|
rows: unknown[],
|
|
@@ -77,17 +103,18 @@ export function computeRowFingerprint(
|
|
|
77
103
|
engine: MutationTimestampSource,
|
|
78
104
|
keyField: string
|
|
79
105
|
): string {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const parts: string[] = [];
|
|
106
|
+
let hash = hashMix(FNV_OFFSET_BASIS, rows.length);
|
|
83
107
|
for (const row of rows) {
|
|
84
108
|
const r = row as Record<string, unknown>;
|
|
85
109
|
const id = String(r[keyField] ?? '');
|
|
86
110
|
const ts = engine.getMutationTimestamp(table, id);
|
|
87
|
-
|
|
111
|
+
hash = hashString(hash, id);
|
|
112
|
+
hash = hashMix(hash, 0); // separator
|
|
113
|
+
hash = hashTimestamp(hash, ts);
|
|
114
|
+
hash = hashMix(hash, 1); // separator
|
|
88
115
|
}
|
|
89
116
|
|
|
90
|
-
return `${table}:${rows.length}:${
|
|
117
|
+
return `${table}:${rows.length}:${hash.toString(16).padStart(8, '0')}`;
|
|
91
118
|
}
|
|
92
119
|
|
|
93
120
|
/**
|
package/src/sync-loop.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {
|
|
8
|
+
SyncCombinedResponse,
|
|
8
9
|
SyncPullResponse,
|
|
9
10
|
SyncPullSubscriptionResponse,
|
|
10
11
|
SyncPushRequest,
|
|
@@ -244,25 +245,34 @@ async function syncOnceCombined<DB extends SyncClientDb>(
|
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
248
|
+
let combined: SyncCombinedResponse;
|
|
249
|
+
try {
|
|
250
|
+
combined = await transport.sync({
|
|
251
|
+
clientId,
|
|
252
|
+
...(pushRequest && !wsPushResponse
|
|
253
|
+
? {
|
|
254
|
+
push: {
|
|
255
|
+
clientCommitId: pushRequest.clientCommitId,
|
|
256
|
+
operations: pushRequest.operations,
|
|
257
|
+
schemaVersion: pushRequest.schemaVersion,
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
: {}),
|
|
261
|
+
pull: {
|
|
262
|
+
limitCommits: pullState.request.limitCommits,
|
|
263
|
+
limitSnapshotRows: pullState.request.limitSnapshotRows,
|
|
264
|
+
maxSnapshotPages: pullState.request.maxSnapshotPages,
|
|
265
|
+
dedupeRows: pullState.request.dedupeRows,
|
|
266
|
+
subscriptions: pullState.request.subscriptions,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
} catch (err) {
|
|
270
|
+
if (outbox) {
|
|
271
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
272
|
+
await markOutboxCommitPending(db, { id: outbox.id, error: message });
|
|
273
|
+
}
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
266
276
|
|
|
267
277
|
// Process push response
|
|
268
278
|
let pushedCommits = 0;
|