@memrosetta/sync-client 0.1.3 → 0.1.5

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 CHANGED
@@ -32,6 +32,7 @@ declare class Inbox {
32
32
  * - `relation_created` INSERT OR IGNORE on the (src, dst, type) PK
33
33
  * - `memory_invalidated` UPDATE of `invalidated_at`
34
34
  * - `feedback_given` additive UPDATE of `use_count` / `success_count`
35
+ * plus the same salience recompute rule used by core
35
36
  * - `memory_tier_set` UPDATE of `tier`
36
37
  */
37
38
  interface ApplyResult {
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ var Outbox = class {
48
48
  addOp(op) {
49
49
  const payloadStr = typeof op.payload === "string" ? op.payload : JSON.stringify(op.payload);
50
50
  this.db.prepare(
51
- `INSERT INTO sync_outbox (op_id, op_type, device_id, user_id, payload, created_at, pushed_at)
51
+ `INSERT OR IGNORE INTO sync_outbox (op_id, op_type, device_id, user_id, payload, created_at, pushed_at)
52
52
  VALUES (?, ?, ?, ?, ?, ?, ?)`
53
53
  ).run(op.opId, op.opType, op.deviceId, op.userId, payloadStr, op.createdAt, null);
54
54
  }
@@ -119,6 +119,10 @@ function parsePayload(payload) {
119
119
  }
120
120
  return payload;
121
121
  }
122
+ function serializeKeywords(keywords) {
123
+ if (!keywords || keywords.length === 0) return null;
124
+ return keywords.join(" ");
125
+ }
122
126
  function applyMemoryCreated(db, op) {
123
127
  const p = parsePayload(op.payload);
124
128
  const stmt = db.prepare(
@@ -148,7 +152,7 @@ function applyMemoryCreated(db, op) {
148
152
  source_id: p.sourceId ?? null,
149
153
  confidence: p.confidence ?? 1,
150
154
  salience: p.salience ?? 1,
151
- keywords: p.keywords ? JSON.stringify(p.keywords) : null,
155
+ keywords: serializeKeywords(p.keywords),
152
156
  event_date_start: p.eventDateStart ?? null,
153
157
  event_date_end: p.eventDateEnd ?? null,
154
158
  invalidated_at: p.invalidatedAt ?? null
@@ -184,6 +188,17 @@ function applyFeedbackGiven(db, op) {
184
188
  "UPDATE memories SET use_count = use_count + 1 WHERE memory_id = ?"
185
189
  ).run(p.memoryId);
186
190
  }
191
+ const row = db.prepare(
192
+ "SELECT use_count, success_count FROM memories WHERE memory_id = ?"
193
+ ).get(p.memoryId);
194
+ if (row && row.use_count > 0) {
195
+ const successRate = row.success_count / row.use_count;
196
+ const newSalience = Math.min(1, Math.max(0.1, 0.5 + 0.5 * successRate));
197
+ db.prepare("UPDATE memories SET salience = ? WHERE memory_id = ?").run(
198
+ newSalience,
199
+ p.memoryId
200
+ );
201
+ }
187
202
  }
188
203
  function applyMemoryTierSet(db, op) {
189
204
  const p = parsePayload(op.payload);
@@ -232,6 +247,7 @@ function applyInboxOps(db, ops) {
232
247
  }
233
248
 
234
249
  // src/sync-client.ts
250
+ var MAX_OPS_PER_PUSH = 400;
235
251
  var SyncClient = class {
236
252
  db;
237
253
  config;
@@ -278,36 +294,47 @@ var SyncClient = class {
278
294
  this.setState("last_push_success_at", now);
279
295
  return { pushed: 0, results: [], highWatermark: 0 };
280
296
  }
281
- const baseCursor = this.getCursor();
282
- const wireOps = pending.map((op) => ({
283
- ...op,
284
- payload: typeof op.payload === "string" ? JSON.parse(op.payload) : op.payload
285
- }));
286
297
  const url = `${this.config.serverUrl}/sync/push`;
287
- const response = await fetch(url, {
288
- method: "POST",
289
- headers: {
290
- "Content-Type": "application/json",
291
- Authorization: `Bearer ${this.config.apiKey}`
292
- },
293
- body: JSON.stringify({
294
- deviceId: this.config.deviceId,
295
- baseCursor,
296
- ops: wireOps
297
- })
298
- });
299
- if (!response.ok) {
300
- throw new Error(`Push failed: ${response.status} ${response.statusText}`);
298
+ const aggregatedResults = [];
299
+ let totalPushed = 0;
300
+ let highWatermark = 0;
301
+ for (let start = 0; start < pending.length; start += MAX_OPS_PER_PUSH) {
302
+ const chunk = pending.slice(start, start + MAX_OPS_PER_PUSH);
303
+ const baseCursor = this.getCursor();
304
+ const wireOps = chunk.map((op) => ({
305
+ ...op,
306
+ payload: typeof op.payload === "string" ? JSON.parse(op.payload) : op.payload
307
+ }));
308
+ const response = await fetch(url, {
309
+ method: "POST",
310
+ headers: {
311
+ "Content-Type": "application/json",
312
+ Authorization: `Bearer ${this.config.apiKey}`
313
+ },
314
+ body: JSON.stringify({
315
+ deviceId: this.config.deviceId,
316
+ baseCursor,
317
+ ops: wireOps
318
+ })
319
+ });
320
+ if (!response.ok) {
321
+ throw new Error(
322
+ `Push failed: ${response.status} ${response.statusText}`
323
+ );
324
+ }
325
+ const body = await response.json();
326
+ const { results, highWatermark: batchHigh } = body.data;
327
+ const pushedIds = results.filter((r) => r.status === "accepted" || r.status === "duplicate").map((r) => r.opId);
328
+ this.outbox.markPushed(pushedIds);
329
+ this.setCursor(batchHigh);
330
+ aggregatedResults.push(...results);
331
+ totalPushed += pushedIds.length;
332
+ highWatermark = batchHigh;
301
333
  }
302
- const body = await response.json();
303
- const { results, highWatermark } = body.data;
304
- const pushedIds = results.filter((r) => r.status === "accepted" || r.status === "duplicate").map((r) => r.opId);
305
- this.outbox.markPushed(pushedIds);
306
- this.setCursor(highWatermark);
307
334
  this.setState("last_push_success_at", (/* @__PURE__ */ new Date()).toISOString());
308
335
  return {
309
- pushed: pushedIds.length,
310
- results,
336
+ pushed: totalPushed,
337
+ results: aggregatedResults,
311
338
  highWatermark
312
339
  };
313
340
  }
@@ -334,14 +361,26 @@ var SyncClient = class {
334
361
  this.inbox.addOps(ops);
335
362
  }
336
363
  const pending = this.inbox.getPending();
364
+ let skippedCount = 0;
337
365
  if (pending.length > 0) {
338
366
  const result = applyInboxOps(this.db, pending);
339
367
  if (result.applied.length > 0) {
340
368
  this.inbox.markApplied(result.applied);
341
369
  }
370
+ skippedCount = result.skipped.length;
371
+ if (skippedCount > 0) {
372
+ for (const s of result.skipped) {
373
+ process.stderr.write(
374
+ `[sync] apply skipped op ${s.opId}: ${s.reason}
375
+ `
376
+ );
377
+ }
378
+ }
342
379
  }
343
380
  this.setCursor(nextCursor);
344
- this.setState("last_pull_success_at", (/* @__PURE__ */ new Date()).toISOString());
381
+ if (skippedCount === 0) {
382
+ this.setState("last_pull_success_at", (/* @__PURE__ */ new Date()).toISOString());
383
+ }
345
384
  return ops.length;
346
385
  }
347
386
  /** For tests / advanced callers: apply currently-pending inbox ops manually. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memrosetta/sync-client",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Local-first sync client for MemRosetta (outbox/inbox, push/pull)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",