@syncular/client 0.0.6-184 → 0.0.6-188
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/client.d.ts +14 -71
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +81 -406
- package/dist/client.js.map +1 -1
- package/dist/create-client.d.ts +0 -2
- package/dist/create-client.d.ts.map +1 -1
- package/dist/create-client.js +2 -3
- package/dist/create-client.js.map +1 -1
- package/dist/engine/SyncEngine.d.ts +1 -0
- package/dist/engine/SyncEngine.d.ts.map +1 -1
- package/dist/engine/SyncEngine.js +21 -6
- package/dist/engine/SyncEngine.js.map +1 -1
- package/dist/handlers/create-handler.d.ts +5 -0
- package/dist/handlers/create-handler.d.ts.map +1 -1
- package/dist/handlers/create-handler.js +123 -4
- package/dist/handlers/create-handler.js.map +1 -1
- package/dist/handlers/types.d.ts +7 -0
- package/dist/handlers/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/blob-schema.d.ts +32 -0
- package/dist/internal/blob-schema.d.ts.map +1 -0
- package/dist/internal/blob-schema.js +2 -0
- package/dist/internal/blob-schema.js.map +1 -0
- package/dist/mutations.d.ts.map +1 -1
- package/dist/mutations.js +15 -6
- package/dist/mutations.js.map +1 -1
- package/dist/plugins/incrementing-version.d.ts.map +1 -1
- package/dist/plugins/incrementing-version.js +20 -8
- package/dist/plugins/incrementing-version.js.map +1 -1
- package/dist/plugins/types.d.ts +26 -1
- package/dist/plugins/types.d.ts.map +1 -1
- package/dist/plugins/types.js.map +1 -1
- package/dist/pull-engine.d.ts +8 -2
- package/dist/pull-engine.d.ts.map +1 -1
- package/dist/pull-engine.js +150 -26
- package/dist/pull-engine.js.map +1 -1
- package/dist/push-engine.d.ts.map +1 -1
- package/dist/push-engine.js +21 -5
- package/dist/push-engine.js.map +1 -1
- package/dist/schema.d.ts +2 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/sync-loop.d.ts +3 -1
- package/dist/sync-loop.d.ts.map +1 -1
- package/dist/sync-loop.js +382 -139
- package/dist/sync-loop.js.map +1 -1
- package/package.json +76 -3
- package/src/client.test.ts +72 -155
- package/src/client.ts +113 -572
- package/src/create-client.ts +1 -6
- package/src/engine/SyncEngine.test.ts +90 -0
- package/src/engine/SyncEngine.ts +29 -9
- package/src/handlers/create-handler.ts +197 -4
- package/src/handlers/types.ts +11 -0
- package/src/index.ts +1 -2
- package/src/internal/blob-schema.ts +40 -0
- package/src/mutations.ts +17 -6
- package/src/plugins/incrementing-version.ts +36 -7
- package/src/plugins/types.ts +42 -0
- package/src/pull-engine.test.ts +494 -0
- package/src/pull-engine.ts +193 -29
- package/src/push-engine.ts +31 -5
- package/src/schema.ts +2 -2
- package/src/sync-loop.ts +538 -145
- package/dist/blobs/index.d.ts +0 -6
- package/dist/blobs/index.d.ts.map +0 -1
- package/dist/blobs/index.js +0 -6
- package/dist/blobs/index.js.map +0 -1
- package/dist/blobs/migrate.d.ts +0 -14
- package/dist/blobs/migrate.d.ts.map +0 -1
- package/dist/blobs/migrate.js +0 -59
- package/dist/blobs/migrate.js.map +0 -1
- package/dist/blobs/types.d.ts +0 -62
- package/dist/blobs/types.d.ts.map +0 -1
- package/dist/blobs/types.js +0 -5
- package/dist/blobs/types.js.map +0 -1
- package/src/blobs/index.ts +0 -6
- package/src/blobs/migrate.ts +0 -67
- package/src/blobs/types.ts +0 -84
package/dist/sync-loop.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { upsertConflictsForRejectedCommit } from './conflicts.js';
|
|
7
7
|
import { getNextSendableOutboxCommit, markOutboxCommitAcked, markOutboxCommitFailed, markOutboxCommitPending, } from './outbox.js';
|
|
8
|
+
import { INCREMENTING_VERSION_PLUGIN_KIND } from './plugins/incrementing-version.js';
|
|
8
9
|
import { applyPullResponse, buildPullRequest, createFollowupPullState, syncPullOnce, } from './pull-engine.js';
|
|
9
|
-
import { syncPushOnce } from './push-engine.js';
|
|
10
10
|
function firstPushErrorCode(response) {
|
|
11
11
|
const firstError = response.results.find((result) => result.status === 'error');
|
|
12
12
|
if (firstError &&
|
|
@@ -29,21 +29,239 @@ function buildPushResult(args) {
|
|
|
29
29
|
timestamp: Date.now(),
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
+
function normalizeError(error) {
|
|
33
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
34
|
+
}
|
|
35
|
+
function createPushRequest(clientId, outboxCommit) {
|
|
36
|
+
return {
|
|
37
|
+
clientId,
|
|
38
|
+
clientCommitId: outboxCommit.client_commit_id,
|
|
39
|
+
operations: outboxCommit.operations,
|
|
40
|
+
schemaVersion: outboxCommit.schema_version,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function toSyncPushResponse(response) {
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
status: response.status,
|
|
47
|
+
commitSeq: response.commitSeq,
|
|
48
|
+
results: response.results,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function isRetriablePushResponse(response) {
|
|
52
|
+
const errorResults = response.results.filter((result) => result.status === 'error');
|
|
53
|
+
return (errorResults.length > 0 &&
|
|
54
|
+
errorResults.every((result) => result.retriable === true));
|
|
55
|
+
}
|
|
56
|
+
async function claimSendableOutboxCommits(db, maxCommits) {
|
|
57
|
+
const claimed = [];
|
|
58
|
+
for (let i = 0; i < maxCommits; i++) {
|
|
59
|
+
const next = await getNextSendableOutboxCommit(db);
|
|
60
|
+
if (!next)
|
|
61
|
+
break;
|
|
62
|
+
claimed.push(next);
|
|
63
|
+
}
|
|
64
|
+
return claimed;
|
|
65
|
+
}
|
|
66
|
+
async function markClaimedOutboxCommitsPending(db, outboxCommits, error) {
|
|
67
|
+
for (const outboxCommit of outboxCommits) {
|
|
68
|
+
await markOutboxCommitPending(db, {
|
|
69
|
+
id: outboxCommit.id,
|
|
70
|
+
error,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function hasIncrementingVersionPlugin(plugins) {
|
|
75
|
+
return plugins.some((plugin) => plugin.kind === INCREMENTING_VERSION_PLUGIN_KIND ||
|
|
76
|
+
plugin.name === INCREMENTING_VERSION_PLUGIN_KIND);
|
|
77
|
+
}
|
|
78
|
+
function makeSequentialRowKey(operation) {
|
|
79
|
+
return `${operation.table}\u001f${operation.row_id}`;
|
|
80
|
+
}
|
|
81
|
+
function advanceSequentialBaseVersionsInBatch(preparedCommits) {
|
|
82
|
+
const nextExpectedBaseVersionByRow = new Map();
|
|
83
|
+
return preparedCommits.map((preparedCommit) => {
|
|
84
|
+
let requestChanged = false;
|
|
85
|
+
const operations = preparedCommit.request.operations.map((operation) => {
|
|
86
|
+
const key = makeSequentialRowKey(operation);
|
|
87
|
+
const nextExpected = nextExpectedBaseVersionByRow.get(key);
|
|
88
|
+
const baseVersion = typeof operation.base_version === 'number' &&
|
|
89
|
+
typeof nextExpected === 'number' &&
|
|
90
|
+
nextExpected > operation.base_version
|
|
91
|
+
? nextExpected
|
|
92
|
+
: operation.base_version;
|
|
93
|
+
const rewrittenOperation = baseVersion === operation.base_version
|
|
94
|
+
? operation
|
|
95
|
+
: { ...operation, base_version: baseVersion };
|
|
96
|
+
if (rewrittenOperation !== operation) {
|
|
97
|
+
requestChanged = true;
|
|
98
|
+
}
|
|
99
|
+
if (operation.op === 'delete') {
|
|
100
|
+
nextExpectedBaseVersionByRow.delete(key);
|
|
101
|
+
return rewrittenOperation;
|
|
102
|
+
}
|
|
103
|
+
if (typeof baseVersion === 'number') {
|
|
104
|
+
nextExpectedBaseVersionByRow.set(key, baseVersion + 1);
|
|
105
|
+
return rewrittenOperation;
|
|
106
|
+
}
|
|
107
|
+
nextExpectedBaseVersionByRow.set(key, 1);
|
|
108
|
+
return rewrittenOperation;
|
|
109
|
+
});
|
|
110
|
+
if (!requestChanged)
|
|
111
|
+
return preparedCommit;
|
|
112
|
+
return {
|
|
113
|
+
...preparedCommit,
|
|
114
|
+
request: {
|
|
115
|
+
...preparedCommit.request,
|
|
116
|
+
operations,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async function preparePushCommits(outboxCommits, options) {
|
|
122
|
+
const plugins = options.plugins ?? [];
|
|
123
|
+
const ctx = {
|
|
124
|
+
actorId: options.actorId ?? 'unknown',
|
|
125
|
+
clientId: options.clientId,
|
|
126
|
+
};
|
|
127
|
+
const prepared = [];
|
|
128
|
+
for (const outboxCommit of outboxCommits) {
|
|
129
|
+
let request = createPushRequest(options.clientId, outboxCommit);
|
|
130
|
+
for (const plugin of plugins) {
|
|
131
|
+
if (!plugin.beforePush)
|
|
132
|
+
continue;
|
|
133
|
+
request = await plugin.beforePush(ctx, request);
|
|
134
|
+
}
|
|
135
|
+
prepared.push({ outboxCommit, request });
|
|
136
|
+
}
|
|
137
|
+
if (!hasIncrementingVersionPlugin(plugins)) {
|
|
138
|
+
return prepared;
|
|
139
|
+
}
|
|
140
|
+
return advanceSequentialBaseVersionsInBatch(prepared);
|
|
141
|
+
}
|
|
142
|
+
async function finalizePushCommit(db, options, preparedCommit, rawResponse) {
|
|
143
|
+
const plugins = options.plugins ?? [];
|
|
144
|
+
const ctx = {
|
|
145
|
+
actorId: options.actorId ?? 'unknown',
|
|
146
|
+
clientId: options.clientId,
|
|
147
|
+
};
|
|
148
|
+
let response = rawResponse;
|
|
149
|
+
let afterPushError = null;
|
|
150
|
+
try {
|
|
151
|
+
for (const plugin of plugins) {
|
|
152
|
+
if (!plugin.afterPush)
|
|
153
|
+
continue;
|
|
154
|
+
response = await plugin.afterPush(ctx, {
|
|
155
|
+
request: preparedCommit.request,
|
|
156
|
+
response,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
afterPushError = normalizeError(error);
|
|
162
|
+
response = rawResponse;
|
|
163
|
+
}
|
|
164
|
+
const responseJson = JSON.stringify(response);
|
|
165
|
+
let status;
|
|
166
|
+
if (response.status === 'applied' || response.status === 'cached') {
|
|
167
|
+
await markOutboxCommitAcked(db, {
|
|
168
|
+
id: preparedCommit.outboxCommit.id,
|
|
169
|
+
commitSeq: response.commitSeq ?? null,
|
|
170
|
+
responseJson,
|
|
171
|
+
});
|
|
172
|
+
status = response.status;
|
|
173
|
+
}
|
|
174
|
+
else if (isRetriablePushResponse(response)) {
|
|
175
|
+
const errorMessages = response.results
|
|
176
|
+
.filter((result) => result.status === 'error')
|
|
177
|
+
.map((result) => result.error ?? 'Unknown error')
|
|
178
|
+
.join('; ');
|
|
179
|
+
await markOutboxCommitPending(db, {
|
|
180
|
+
id: preparedCommit.outboxCommit.id,
|
|
181
|
+
error: `Retriable: ${errorMessages}`,
|
|
182
|
+
responseJson,
|
|
183
|
+
});
|
|
184
|
+
status = 'retriable';
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
await upsertConflictsForRejectedCommit(db, {
|
|
188
|
+
outboxCommitId: preparedCommit.outboxCommit.id,
|
|
189
|
+
clientCommitId: preparedCommit.outboxCommit.client_commit_id,
|
|
190
|
+
response,
|
|
191
|
+
});
|
|
192
|
+
await markOutboxCommitFailed(db, {
|
|
193
|
+
id: preparedCommit.outboxCommit.id,
|
|
194
|
+
error: 'REJECTED',
|
|
195
|
+
responseJson,
|
|
196
|
+
});
|
|
197
|
+
status = 'rejected';
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
pushResult: buildPushResult({
|
|
201
|
+
outboxCommitId: preparedCommit.outboxCommit.id,
|
|
202
|
+
clientCommitId: preparedCommit.outboxCommit.client_commit_id,
|
|
203
|
+
status,
|
|
204
|
+
response,
|
|
205
|
+
}),
|
|
206
|
+
afterPushError,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
32
209
|
async function syncPushUntilSettled(db, transport, options) {
|
|
33
210
|
const maxCommits = Math.max(1, Math.min(1000, options.maxCommits ?? 20));
|
|
34
211
|
let pushedCount = 0;
|
|
35
212
|
const pushResults = [];
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
actorId: options.actorId,
|
|
40
|
-
plugins: options.plugins,
|
|
41
|
-
});
|
|
42
|
-
if (!res.pushed)
|
|
213
|
+
while (pushedCount < maxCommits) {
|
|
214
|
+
const claimedCommits = await claimSendableOutboxCommits(db, maxCommits - pushedCount);
|
|
215
|
+
if (claimedCommits.length === 0)
|
|
43
216
|
break;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
217
|
+
let preparedCommits;
|
|
218
|
+
try {
|
|
219
|
+
preparedCommits = await preparePushCommits(claimedCommits, options);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
const normalizedError = normalizeError(error);
|
|
223
|
+
await markClaimedOutboxCommitsPending(db, claimedCommits, normalizedError.message);
|
|
224
|
+
throw normalizedError;
|
|
225
|
+
}
|
|
226
|
+
let combined;
|
|
227
|
+
try {
|
|
228
|
+
combined = await transport.sync({
|
|
229
|
+
clientId: options.clientId,
|
|
230
|
+
push: {
|
|
231
|
+
commits: preparedCommits.map(({ request }) => ({
|
|
232
|
+
clientCommitId: request.clientCommitId,
|
|
233
|
+
operations: request.operations,
|
|
234
|
+
schemaVersion: request.schemaVersion,
|
|
235
|
+
})),
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
const normalizedError = normalizeError(error);
|
|
241
|
+
await markClaimedOutboxCommitsPending(db, claimedCommits, normalizedError.message);
|
|
242
|
+
throw normalizedError;
|
|
243
|
+
}
|
|
244
|
+
const batchResponses = combined.push?.commits ?? [];
|
|
245
|
+
const responsesByClientCommitId = new Map(batchResponses.map((response) => [response.clientCommitId, response]));
|
|
246
|
+
if (!combined.push ||
|
|
247
|
+
preparedCommits.some(({ request }) => !responsesByClientCommitId.has(request.clientCommitId))) {
|
|
248
|
+
await markClaimedOutboxCommitsPending(db, claimedCommits, 'MISSING_PUSH_RESPONSE');
|
|
249
|
+
throw new Error('Server returned incomplete push response');
|
|
250
|
+
}
|
|
251
|
+
let deferredError = null;
|
|
252
|
+
for (const preparedCommit of preparedCommits) {
|
|
253
|
+
const batchResponse = responsesByClientCommitId.get(preparedCommit.request.clientCommitId);
|
|
254
|
+
if (!batchResponse)
|
|
255
|
+
continue;
|
|
256
|
+
const finalized = await finalizePushCommit(db, options, preparedCommit, toSyncPushResponse(batchResponse));
|
|
257
|
+
pushResults.push(finalized.pushResult);
|
|
258
|
+
pushedCount += 1;
|
|
259
|
+
if (!deferredError && finalized.afterPushError) {
|
|
260
|
+
deferredError = finalized.afterPushError;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (deferredError) {
|
|
264
|
+
throw deferredError;
|
|
47
265
|
}
|
|
48
266
|
}
|
|
49
267
|
return { pushedCount, pushResults };
|
|
@@ -51,16 +269,16 @@ async function syncPushUntilSettled(db, transport, options) {
|
|
|
51
269
|
function hasPushViaWs(transport) {
|
|
52
270
|
return 'pushViaWs' in transport && typeof transport.pushViaWs === 'function';
|
|
53
271
|
}
|
|
54
|
-
function needsAnotherPull(res) {
|
|
272
|
+
function needsAnotherPull(res, limitCommits) {
|
|
273
|
+
let totalCommits = 0;
|
|
55
274
|
for (const sub of res.subscriptions ?? []) {
|
|
56
275
|
if (sub.status !== 'active')
|
|
57
276
|
continue;
|
|
58
277
|
if (sub.bootstrap)
|
|
59
278
|
return true;
|
|
60
|
-
|
|
61
|
-
return true;
|
|
279
|
+
totalCommits += sub.commits?.length ?? 0;
|
|
62
280
|
}
|
|
63
|
-
return
|
|
281
|
+
return totalCommits >= limitCommits;
|
|
64
282
|
}
|
|
65
283
|
function mergePullResponse(targetBySubId, res) {
|
|
66
284
|
for (const sub of res.subscriptions ?? []) {
|
|
@@ -83,6 +301,30 @@ function mergePullResponse(targetBySubId, res) {
|
|
|
83
301
|
targetBySubId.set(sub.id, merged);
|
|
84
302
|
}
|
|
85
303
|
}
|
|
304
|
+
function canSkipPullAfterLocalWsPush(pullState, pushResponse, options) {
|
|
305
|
+
if (!options.allowSkipPullOnLocalWsPush)
|
|
306
|
+
return false;
|
|
307
|
+
if (options.trigger !== 'local')
|
|
308
|
+
return false;
|
|
309
|
+
if (!pushResponse)
|
|
310
|
+
return false;
|
|
311
|
+
if (pushResponse.status !== 'applied' && pushResponse.status !== 'cached') {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
if ((options.plugins ?? []).some((plugin) => typeof plugin.afterPull === 'function')) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
for (const subscription of pullState.request.subscriptions ?? []) {
|
|
318
|
+
if (subscription.bootstrapState != null) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
const cursor = subscription.cursor ?? -1;
|
|
322
|
+
if (cursor < 0) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
86
328
|
async function syncPullUntilSettled(db, transport, handlers, options) {
|
|
87
329
|
const maxRounds = Math.max(1, Math.min(1000, options.maxRounds ?? 20));
|
|
88
330
|
const aggregatedBySubId = new Map();
|
|
@@ -92,7 +334,7 @@ async function syncPullUntilSettled(db, transport, handlers, options) {
|
|
|
92
334
|
rounds += 1;
|
|
93
335
|
const res = await syncPullOnce(db, transport, handlers, options, pullState);
|
|
94
336
|
mergePullResponse(aggregatedBySubId, res);
|
|
95
|
-
if (!needsAnotherPull(res))
|
|
337
|
+
if (!needsAnotherPull(res, pullState.request.limitCommits))
|
|
96
338
|
break;
|
|
97
339
|
pullState = createFollowupPullState(pullState, res);
|
|
98
340
|
}
|
|
@@ -131,161 +373,162 @@ async function syncOnceCombined(db, transport, handlers, options) {
|
|
|
131
373
|
// Build pull request (reads subscription state)
|
|
132
374
|
const pullState = await buildPullRequest(db, pullOpts);
|
|
133
375
|
const { clientId } = pullState.request;
|
|
134
|
-
// Grab at most one outbox commit
|
|
135
|
-
const outbox = await getNextSendableOutboxCommit(db);
|
|
136
376
|
const plugins = options.plugins ?? [];
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
clientId,
|
|
140
|
-
};
|
|
141
|
-
// Build push request, running beforePush plugins
|
|
142
|
-
let pushRequest;
|
|
377
|
+
const outbox = await getNextSendableOutboxCommit(db);
|
|
378
|
+
let preparedFirstCommit = null;
|
|
143
379
|
if (outbox) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
380
|
+
try {
|
|
381
|
+
const preparedCommits = await preparePushCommits([outbox], {
|
|
382
|
+
clientId: options.clientId,
|
|
383
|
+
actorId: options.actorId,
|
|
384
|
+
plugins,
|
|
385
|
+
});
|
|
386
|
+
preparedFirstCommit = preparedCommits[0] ?? null;
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
const normalizedError = normalizeError(error);
|
|
390
|
+
await markClaimedOutboxCommitsPending(db, [outbox], normalizedError.message);
|
|
391
|
+
throw normalizedError;
|
|
154
392
|
}
|
|
155
393
|
}
|
|
156
394
|
// Try WS push first for the first outbox commit (if realtime transport supports it).
|
|
157
395
|
// Fall back to HTTP push in the combined request when WS is unavailable or fails.
|
|
158
396
|
let wsPushResponse = null;
|
|
159
|
-
if (
|
|
397
|
+
if (preparedFirstCommit && hasPushViaWs(transport)) {
|
|
160
398
|
try {
|
|
161
|
-
wsPushResponse = await transport.pushViaWs(
|
|
399
|
+
wsPushResponse = await transport.pushViaWs(preparedFirstCommit.request);
|
|
162
400
|
}
|
|
163
401
|
catch {
|
|
164
402
|
wsPushResponse = null;
|
|
165
403
|
}
|
|
166
404
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
405
|
+
const skipPullAfterWsPush = canSkipPullAfterLocalWsPush(pullState, wsPushResponse, options);
|
|
406
|
+
let combined = null;
|
|
407
|
+
let combinedPushCommits = [];
|
|
408
|
+
if (!skipPullAfterWsPush) {
|
|
409
|
+
combinedPushCommits =
|
|
410
|
+
preparedFirstCommit && !wsPushResponse
|
|
411
|
+
? [
|
|
412
|
+
preparedFirstCommit,
|
|
413
|
+
...(await (async () => {
|
|
414
|
+
const additionalOutboxCommits = await claimSendableOutboxCommits(db, Math.max(0, (options.maxPushCommits ?? 20) - 1));
|
|
415
|
+
if (additionalOutboxCommits.length === 0)
|
|
416
|
+
return [];
|
|
417
|
+
try {
|
|
418
|
+
return await preparePushCommits(additionalOutboxCommits, {
|
|
419
|
+
clientId: options.clientId,
|
|
420
|
+
actorId: options.actorId,
|
|
421
|
+
plugins,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
const normalizedError = normalizeError(error);
|
|
426
|
+
await markClaimedOutboxCommitsPending(db, additionalOutboxCommits, normalizedError.message);
|
|
427
|
+
throw normalizedError;
|
|
428
|
+
}
|
|
429
|
+
})()),
|
|
430
|
+
]
|
|
431
|
+
: [];
|
|
432
|
+
if (hasIncrementingVersionPlugin(plugins)) {
|
|
433
|
+
combinedPushCommits =
|
|
434
|
+
advanceSequentialBaseVersionsInBatch(combinedPushCommits);
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
combined = await transport.sync({
|
|
438
|
+
clientId,
|
|
439
|
+
...(combinedPushCommits.length > 0
|
|
440
|
+
? {
|
|
441
|
+
push: {
|
|
442
|
+
commits: combinedPushCommits.map(({ request }) => ({
|
|
443
|
+
clientCommitId: request.clientCommitId,
|
|
444
|
+
operations: request.operations,
|
|
445
|
+
schemaVersion: request.schemaVersion,
|
|
446
|
+
})),
|
|
447
|
+
},
|
|
448
|
+
}
|
|
449
|
+
: {}),
|
|
450
|
+
pull: {
|
|
451
|
+
limitCommits: pullState.request.limitCommits,
|
|
452
|
+
limitSnapshotRows: pullState.request.limitSnapshotRows,
|
|
453
|
+
maxSnapshotPages: pullState.request.maxSnapshotPages,
|
|
454
|
+
dedupeRows: pullState.request.dedupeRows,
|
|
455
|
+
subscriptions: pullState.request.subscriptions,
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
if (combinedPushCommits.length > 0) {
|
|
461
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
462
|
+
await markClaimedOutboxCommitsPending(db, combinedPushCommits.map(({ outboxCommit }) => outboxCommit), message);
|
|
463
|
+
}
|
|
464
|
+
throw err;
|
|
193
465
|
}
|
|
194
|
-
throw err;
|
|
195
466
|
}
|
|
196
467
|
// Process push response
|
|
197
468
|
let pushedCommits = 0;
|
|
198
469
|
const pushResults = [];
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
// Run afterPush plugins
|
|
209
|
-
for (const plugin of plugins) {
|
|
210
|
-
if (!plugin.afterPush)
|
|
211
|
-
continue;
|
|
212
|
-
pushRes = await plugin.afterPush(ctx, {
|
|
213
|
-
request: pushRequest,
|
|
214
|
-
response: pushRes,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
const responseJson = JSON.stringify(pushRes);
|
|
218
|
-
if (pushRes.status === 'applied' || pushRes.status === 'cached') {
|
|
219
|
-
await markOutboxCommitAcked(db, {
|
|
220
|
-
id: outbox.id,
|
|
221
|
-
commitSeq: pushRes.commitSeq ?? null,
|
|
222
|
-
responseJson,
|
|
223
|
-
});
|
|
224
|
-
pushResults.push(buildPushResult({
|
|
225
|
-
outboxCommitId: outbox.id,
|
|
226
|
-
clientCommitId: outbox.client_commit_id,
|
|
227
|
-
status: pushRes.status,
|
|
228
|
-
response: pushRes,
|
|
229
|
-
}));
|
|
470
|
+
if (preparedFirstCommit) {
|
|
471
|
+
if (wsPushResponse) {
|
|
472
|
+
const finalizedFirstCommit = await finalizePushCommit(db, {
|
|
473
|
+
clientId: options.clientId,
|
|
474
|
+
actorId: options.actorId,
|
|
475
|
+
plugins,
|
|
476
|
+
}, preparedFirstCommit, wsPushResponse);
|
|
477
|
+
pushResults.push(finalizedFirstCommit.pushResult);
|
|
230
478
|
pushedCommits = 1;
|
|
479
|
+
if (finalizedFirstCommit.afterPushError) {
|
|
480
|
+
throw finalizedFirstCommit.afterPushError;
|
|
481
|
+
}
|
|
231
482
|
}
|
|
232
483
|
else {
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
484
|
+
const batchResponses = combined?.push?.commits ?? [];
|
|
485
|
+
const responsesByClientCommitId = new Map(batchResponses.map((response) => [response.clientCommitId, response]));
|
|
486
|
+
if (!combined?.push ||
|
|
487
|
+
combinedPushCommits.some(({ request }) => !responsesByClientCommitId.has(request.clientCommitId))) {
|
|
488
|
+
await markClaimedOutboxCommitsPending(db, combinedPushCommits.map(({ outboxCommit }) => outboxCommit), 'MISSING_PUSH_RESPONSE');
|
|
489
|
+
throw new Error('Server returned incomplete push response');
|
|
490
|
+
}
|
|
491
|
+
let deferredError = null;
|
|
492
|
+
for (const preparedCommit of combinedPushCommits) {
|
|
493
|
+
const batchResponse = responsesByClientCommitId.get(preparedCommit.request.clientCommitId);
|
|
494
|
+
if (!batchResponse)
|
|
495
|
+
continue;
|
|
496
|
+
const finalizedCommit = await finalizePushCommit(db, {
|
|
497
|
+
clientId: options.clientId,
|
|
498
|
+
actorId: options.actorId,
|
|
499
|
+
plugins,
|
|
500
|
+
}, preparedCommit, toSyncPushResponse(batchResponse));
|
|
501
|
+
pushResults.push(finalizedCommit.pushResult);
|
|
502
|
+
pushedCommits += 1;
|
|
503
|
+
if (!deferredError && finalizedCommit.afterPushError) {
|
|
504
|
+
deferredError = finalizedCommit.afterPushError;
|
|
505
|
+
}
|
|
250
506
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
outboxCommitId: outbox.id,
|
|
254
|
-
clientCommitId: outbox.client_commit_id,
|
|
255
|
-
response: pushRes,
|
|
256
|
-
});
|
|
257
|
-
await markOutboxCommitFailed(db, {
|
|
258
|
-
id: outbox.id,
|
|
259
|
-
error: 'REJECTED',
|
|
260
|
-
responseJson,
|
|
261
|
-
});
|
|
262
|
-
pushResults.push(buildPushResult({
|
|
263
|
-
outboxCommitId: outbox.id,
|
|
264
|
-
clientCommitId: outbox.client_commit_id,
|
|
265
|
-
status: 'rejected',
|
|
266
|
-
response: pushRes,
|
|
267
|
-
}));
|
|
268
|
-
pushedCommits = 1;
|
|
507
|
+
if (deferredError) {
|
|
508
|
+
throw deferredError;
|
|
269
509
|
}
|
|
270
510
|
}
|
|
271
511
|
// Settle remaining outbox commits
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
512
|
+
const remainingMaxCommits = Math.max(0, (options.maxPushCommits ?? 20) - pushedCommits);
|
|
513
|
+
if (remainingMaxCommits > 0) {
|
|
514
|
+
const remaining = await syncPushUntilSettled(db, transport, {
|
|
515
|
+
clientId: options.clientId,
|
|
516
|
+
actorId: options.actorId,
|
|
517
|
+
plugins: options.plugins,
|
|
518
|
+
maxCommits: remainingMaxCommits,
|
|
519
|
+
});
|
|
520
|
+
pushedCommits += remaining.pushedCount;
|
|
521
|
+
pushResults.push(...remaining.pushResults);
|
|
522
|
+
}
|
|
280
523
|
}
|
|
281
524
|
// Process pull response
|
|
282
525
|
let pullResponse = { ok: true, subscriptions: [] };
|
|
283
526
|
let pullRounds = 0;
|
|
284
|
-
if (combined
|
|
527
|
+
if (combined?.pull) {
|
|
285
528
|
pullResponse = await applyPullResponse(db, transport, handlers, pullOpts, pullState, combined.pull);
|
|
286
529
|
pullRounds = 1;
|
|
287
530
|
// Continue pulling if more data
|
|
288
|
-
if (needsAnotherPull(pullResponse)) {
|
|
531
|
+
if (needsAnotherPull(pullResponse, pullState.request.limitCommits)) {
|
|
289
532
|
const aggregatedBySubId = new Map();
|
|
290
533
|
mergePullResponse(aggregatedBySubId, pullResponse);
|
|
291
534
|
const more = await syncPullUntilSettled(db, transport, handlers, {
|