@memrosetta/sync-client 0.1.5 → 0.1.7
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 +63 -40
- 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) {
|
|
@@ -248,6 +263,7 @@ function applyInboxOps(db, ops) {
|
|
|
248
263
|
|
|
249
264
|
// src/sync-client.ts
|
|
250
265
|
var MAX_OPS_PER_PUSH = 400;
|
|
266
|
+
var PULL_PAGE_SIZE = 1e3;
|
|
251
267
|
var SyncClient = class {
|
|
252
268
|
db;
|
|
253
269
|
config;
|
|
@@ -274,7 +290,7 @@ var SyncClient = class {
|
|
|
274
290
|
serverUrl: this.config.serverUrl,
|
|
275
291
|
userId: this.config.userId,
|
|
276
292
|
deviceId: this.config.deviceId,
|
|
277
|
-
pendingOps: this.outbox.countPending(),
|
|
293
|
+
pendingOps: this.outbox.countPending(this.config.userId),
|
|
278
294
|
lastPush: {
|
|
279
295
|
attemptAt: this.getState("last_push_attempt_at"),
|
|
280
296
|
successAt: this.getState("last_push_success_at")
|
|
@@ -289,7 +305,7 @@ var SyncClient = class {
|
|
|
289
305
|
async push() {
|
|
290
306
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
291
307
|
this.setState("last_push_attempt_at", now);
|
|
292
|
-
const pending = this.outbox.getPending();
|
|
308
|
+
const pending = this.outbox.getPending(this.config.userId);
|
|
293
309
|
if (pending.length === 0) {
|
|
294
310
|
this.setState("last_push_success_at", now);
|
|
295
311
|
return { pushed: 0, results: [], highWatermark: 0 };
|
|
@@ -340,48 +356,55 @@ var SyncClient = class {
|
|
|
340
356
|
}
|
|
341
357
|
async pull() {
|
|
342
358
|
this.setState("last_pull_attempt_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
359
|
+
let totalPulled = 0;
|
|
360
|
+
let totalSkipped = 0;
|
|
361
|
+
let hasMore = true;
|
|
362
|
+
while (hasMore) {
|
|
363
|
+
const since = this.getCursor();
|
|
364
|
+
const params = new URLSearchParams({
|
|
365
|
+
since: String(since),
|
|
366
|
+
userId: this.config.userId,
|
|
367
|
+
limit: String(PULL_PAGE_SIZE)
|
|
368
|
+
});
|
|
369
|
+
const url = `${this.config.serverUrl}/sync/pull?${params.toString()}`;
|
|
370
|
+
const response = await fetch(url, {
|
|
371
|
+
method: "GET",
|
|
372
|
+
headers: {
|
|
373
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
if (!response.ok) {
|
|
377
|
+
throw new Error(`Pull failed: ${response.status} ${response.statusText}`);
|
|
353
378
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const { ops, nextCursor } = body.data;
|
|
360
|
-
if (ops.length > 0) {
|
|
361
|
-
this.inbox.addOps(ops);
|
|
362
|
-
}
|
|
363
|
-
const pending = this.inbox.getPending();
|
|
364
|
-
let skippedCount = 0;
|
|
365
|
-
if (pending.length > 0) {
|
|
366
|
-
const result = applyInboxOps(this.db, pending);
|
|
367
|
-
if (result.applied.length > 0) {
|
|
368
|
-
this.inbox.markApplied(result.applied);
|
|
379
|
+
const body = await response.json();
|
|
380
|
+
const { ops, nextCursor } = body.data;
|
|
381
|
+
hasMore = body.data.hasMore ?? false;
|
|
382
|
+
if (ops.length > 0) {
|
|
383
|
+
this.inbox.addOps(ops);
|
|
369
384
|
}
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
385
|
+
const pending = this.inbox.getPending();
|
|
386
|
+
if (pending.length > 0) {
|
|
387
|
+
const result = applyInboxOps(this.db, pending);
|
|
388
|
+
if (result.applied.length > 0) {
|
|
389
|
+
this.inbox.markApplied(result.applied);
|
|
390
|
+
}
|
|
391
|
+
totalSkipped += result.skipped.length;
|
|
392
|
+
if (result.skipped.length > 0) {
|
|
393
|
+
for (const s of result.skipped) {
|
|
394
|
+
process.stderr.write(
|
|
395
|
+
`[sync] apply skipped op ${s.opId}: ${s.reason}
|
|
375
396
|
`
|
|
376
|
-
|
|
397
|
+
);
|
|
398
|
+
}
|
|
377
399
|
}
|
|
378
400
|
}
|
|
401
|
+
this.setCursor(nextCursor);
|
|
402
|
+
totalPulled += ops.length;
|
|
379
403
|
}
|
|
380
|
-
|
|
381
|
-
if (skippedCount === 0) {
|
|
404
|
+
if (totalSkipped === 0) {
|
|
382
405
|
this.setState("last_pull_success_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
383
406
|
}
|
|
384
|
-
return
|
|
407
|
+
return totalPulled;
|
|
385
408
|
}
|
|
386
409
|
/** For tests / advanced callers: apply currently-pending inbox ops manually. */
|
|
387
410
|
applyPendingInbox() {
|