@memrosetta/sync-client 0.1.4 → 0.1.6
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/index.d.ts +15 -2
- package/dist/index.js +59 -32
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -6,8 +6,21 @@ declare class Outbox {
|
|
|
6
6
|
private readonly db;
|
|
7
7
|
constructor(db: Database.Database);
|
|
8
8
|
addOp(op: SyncOp): void;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Return pending outbox ops in chronological order.
|
|
11
|
+
*
|
|
12
|
+
* When `userId` is supplied, only ops belonging to that user are
|
|
13
|
+
* returned. This is the v0.5.2 hardening that prevents a
|
|
14
|
+
* cross-user fragmentation: if an older client wrote ops under a
|
|
15
|
+
* legacy `user_id` and the current `SyncClient` is configured with
|
|
16
|
+
* a canonical user, the transport will no longer silently pick up
|
|
17
|
+
* the legacy ops and ship them to the hub. The legacy queue is
|
|
18
|
+
* cleared by `memrosetta migrate legacy-user-ids` before the first
|
|
19
|
+
* post-migration push so this filter is defense-in-depth, not the
|
|
20
|
+
* primary fix.
|
|
21
|
+
*/
|
|
22
|
+
getPending(userId?: string): readonly SyncOp[];
|
|
23
|
+
countPending(userId?: string): number;
|
|
11
24
|
markPushed(opIds: readonly string[]): void;
|
|
12
25
|
}
|
|
13
26
|
|
package/dist/index.js
CHANGED
|
@@ -52,12 +52,27 @@ var Outbox = class {
|
|
|
52
52
|
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
53
53
|
).run(op.opId, op.opType, op.deviceId, op.userId, payloadStr, op.createdAt, null);
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Return pending outbox ops in chronological order.
|
|
57
|
+
*
|
|
58
|
+
* When `userId` is supplied, only ops belonging to that user are
|
|
59
|
+
* returned. This is the v0.5.2 hardening that prevents a
|
|
60
|
+
* cross-user fragmentation: if an older client wrote ops under a
|
|
61
|
+
* legacy `user_id` and the current `SyncClient` is configured with
|
|
62
|
+
* a canonical user, the transport will no longer silently pick up
|
|
63
|
+
* the legacy ops and ship them to the hub. The legacy queue is
|
|
64
|
+
* cleared by `memrosetta migrate legacy-user-ids` before the first
|
|
65
|
+
* post-migration push so this filter is defense-in-depth, not the
|
|
66
|
+
* primary fix.
|
|
67
|
+
*/
|
|
68
|
+
getPending(userId) {
|
|
69
|
+
const rows = userId && userId.length > 0 ? this.db.prepare(
|
|
70
|
+
"SELECT * FROM sync_outbox WHERE pushed_at IS NULL AND user_id = ? ORDER BY created_at ASC"
|
|
71
|
+
).all(userId) : this.db.prepare("SELECT * FROM sync_outbox WHERE pushed_at IS NULL ORDER BY created_at ASC").all();
|
|
57
72
|
return rows.map(rowToSyncOp);
|
|
58
73
|
}
|
|
59
|
-
countPending() {
|
|
60
|
-
const row = this.db.prepare("SELECT COUNT(*) as count FROM sync_outbox WHERE pushed_at IS NULL").get();
|
|
74
|
+
countPending(userId) {
|
|
75
|
+
const row = userId && userId.length > 0 ? this.db.prepare("SELECT COUNT(*) as count FROM sync_outbox WHERE pushed_at IS NULL AND user_id = ?").get(userId) : this.db.prepare("SELECT COUNT(*) as count FROM sync_outbox WHERE pushed_at IS NULL").get();
|
|
61
76
|
return row.count;
|
|
62
77
|
}
|
|
63
78
|
markPushed(opIds) {
|
|
@@ -247,6 +262,7 @@ function applyInboxOps(db, ops) {
|
|
|
247
262
|
}
|
|
248
263
|
|
|
249
264
|
// src/sync-client.ts
|
|
265
|
+
var MAX_OPS_PER_PUSH = 400;
|
|
250
266
|
var SyncClient = class {
|
|
251
267
|
db;
|
|
252
268
|
config;
|
|
@@ -273,7 +289,7 @@ var SyncClient = class {
|
|
|
273
289
|
serverUrl: this.config.serverUrl,
|
|
274
290
|
userId: this.config.userId,
|
|
275
291
|
deviceId: this.config.deviceId,
|
|
276
|
-
pendingOps: this.outbox.countPending(),
|
|
292
|
+
pendingOps: this.outbox.countPending(this.config.userId),
|
|
277
293
|
lastPush: {
|
|
278
294
|
attemptAt: this.getState("last_push_attempt_at"),
|
|
279
295
|
successAt: this.getState("last_push_success_at")
|
|
@@ -288,41 +304,52 @@ var SyncClient = class {
|
|
|
288
304
|
async push() {
|
|
289
305
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
290
306
|
this.setState("last_push_attempt_at", now);
|
|
291
|
-
const pending = this.outbox.getPending();
|
|
307
|
+
const pending = this.outbox.getPending(this.config.userId);
|
|
292
308
|
if (pending.length === 0) {
|
|
293
309
|
this.setState("last_push_success_at", now);
|
|
294
310
|
return { pushed: 0, results: [], highWatermark: 0 };
|
|
295
311
|
}
|
|
296
|
-
const baseCursor = this.getCursor();
|
|
297
|
-
const wireOps = pending.map((op) => ({
|
|
298
|
-
...op,
|
|
299
|
-
payload: typeof op.payload === "string" ? JSON.parse(op.payload) : op.payload
|
|
300
|
-
}));
|
|
301
312
|
const url = `${this.config.serverUrl}/sync/push`;
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
313
|
+
const aggregatedResults = [];
|
|
314
|
+
let totalPushed = 0;
|
|
315
|
+
let highWatermark = 0;
|
|
316
|
+
for (let start = 0; start < pending.length; start += MAX_OPS_PER_PUSH) {
|
|
317
|
+
const chunk = pending.slice(start, start + MAX_OPS_PER_PUSH);
|
|
318
|
+
const baseCursor = this.getCursor();
|
|
319
|
+
const wireOps = chunk.map((op) => ({
|
|
320
|
+
...op,
|
|
321
|
+
payload: typeof op.payload === "string" ? JSON.parse(op.payload) : op.payload
|
|
322
|
+
}));
|
|
323
|
+
const response = await fetch(url, {
|
|
324
|
+
method: "POST",
|
|
325
|
+
headers: {
|
|
326
|
+
"Content-Type": "application/json",
|
|
327
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
328
|
+
},
|
|
329
|
+
body: JSON.stringify({
|
|
330
|
+
deviceId: this.config.deviceId,
|
|
331
|
+
baseCursor,
|
|
332
|
+
ops: wireOps
|
|
333
|
+
})
|
|
334
|
+
});
|
|
335
|
+
if (!response.ok) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`Push failed: ${response.status} ${response.statusText}`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
const body = await response.json();
|
|
341
|
+
const { results, highWatermark: batchHigh } = body.data;
|
|
342
|
+
const pushedIds = results.filter((r) => r.status === "accepted" || r.status === "duplicate").map((r) => r.opId);
|
|
343
|
+
this.outbox.markPushed(pushedIds);
|
|
344
|
+
this.setCursor(batchHigh);
|
|
345
|
+
aggregatedResults.push(...results);
|
|
346
|
+
totalPushed += pushedIds.length;
|
|
347
|
+
highWatermark = batchHigh;
|
|
316
348
|
}
|
|
317
|
-
const body = await response.json();
|
|
318
|
-
const { results, highWatermark } = body.data;
|
|
319
|
-
const pushedIds = results.filter((r) => r.status === "accepted" || r.status === "duplicate").map((r) => r.opId);
|
|
320
|
-
this.outbox.markPushed(pushedIds);
|
|
321
|
-
this.setCursor(highWatermark);
|
|
322
349
|
this.setState("last_push_success_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
323
350
|
return {
|
|
324
|
-
pushed:
|
|
325
|
-
results,
|
|
351
|
+
pushed: totalPushed,
|
|
352
|
+
results: aggregatedResults,
|
|
326
353
|
highWatermark
|
|
327
354
|
};
|
|
328
355
|
}
|