@memrosetta/sync-client 0.1.1 → 0.1.3

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
@@ -19,6 +19,36 @@ declare class Inbox {
19
19
  markApplied(opIds: readonly string[]): void;
20
20
  }
21
21
 
22
+ /**
23
+ * Applies pulled sync ops to the local SQLite `memories` + `memory_relations`
24
+ * tables.
25
+ *
26
+ * Intentionally separate from `SyncClient` / `Inbox` so the transport layer
27
+ * stays ignorant of the engine schema. Anything that pulls ops from an inbox
28
+ * can call `applyInboxOps(db, ops)` to fold them into the local memory graph.
29
+ *
30
+ * All writes are idempotent:
31
+ * - `memory_created` INSERT OR IGNORE on `memory_id`
32
+ * - `relation_created` INSERT OR IGNORE on the (src, dst, type) PK
33
+ * - `memory_invalidated` UPDATE of `invalidated_at`
34
+ * - `feedback_given` additive UPDATE of `use_count` / `success_count`
35
+ * - `memory_tier_set` UPDATE of `tier`
36
+ */
37
+ interface ApplyResult {
38
+ readonly applied: readonly string[];
39
+ readonly skipped: readonly {
40
+ readonly opId: string;
41
+ readonly reason: string;
42
+ }[];
43
+ }
44
+ /**
45
+ * Apply a batch of pulled ops into the local memories/relations tables.
46
+ *
47
+ * The caller is responsible for passing only ops that have not been applied
48
+ * yet. `Inbox.getPending()` returns exactly that set.
49
+ */
50
+ declare function applyInboxOps(db: Database.Database, ops: readonly SyncPulledOp[]): ApplyResult;
51
+
22
52
  /**
23
53
  * Minimal config required by SyncClient at runtime.
24
54
  * serverUrl and apiKey are required (unlike the optional SyncConfig
@@ -42,6 +72,7 @@ interface SyncStatusTimestamps {
42
72
  interface SyncClientStatus {
43
73
  readonly enabled: true;
44
74
  readonly serverUrl: string;
75
+ readonly userId: string;
45
76
  readonly deviceId: string;
46
77
  readonly pendingOps: number;
47
78
  readonly lastPush: SyncStatusTimestamps;
@@ -60,6 +91,8 @@ declare class SyncClient {
60
91
  getStatus(): SyncClientStatus;
61
92
  push(): Promise<SyncClientPushResponse>;
62
93
  pull(): Promise<number>;
94
+ /** For tests / advanced callers: apply currently-pending inbox ops manually. */
95
+ applyPendingInbox(): ApplyResult;
63
96
  getState(key: string): string | null;
64
97
  setState(key: string, value: string): void;
65
98
  private getCursor;
@@ -68,4 +101,4 @@ declare class SyncClient {
68
101
 
69
102
  declare function ensureSyncSchema(db: Database.Database): void;
70
103
 
71
- export { Inbox, Outbox, SyncClient, type SyncClientConfig, type SyncClientPushResponse, type SyncClientStatus, type SyncStatusTimestamps, ensureSyncSchema };
104
+ export { type ApplyResult, Inbox, Outbox, SyncClient, type SyncClientConfig, type SyncClientPushResponse, type SyncClientStatus, type SyncStatusTimestamps, applyInboxOps, ensureSyncSchema };
package/dist/index.js CHANGED
@@ -112,6 +112,125 @@ var Inbox = class {
112
112
  }
113
113
  };
114
114
 
115
+ // src/applier.ts
116
+ function parsePayload(payload) {
117
+ if (typeof payload === "string") {
118
+ return JSON.parse(payload);
119
+ }
120
+ return payload;
121
+ }
122
+ function applyMemoryCreated(db, op) {
123
+ const p = parsePayload(op.payload);
124
+ const stmt = db.prepare(
125
+ `INSERT OR IGNORE INTO memories (
126
+ memory_id, user_id, namespace, memory_type, content, raw_text,
127
+ document_date, learned_at, source_id, confidence, salience,
128
+ is_latest, keywords, event_date_start, event_date_end,
129
+ invalidated_at, tier, activation_score, access_count,
130
+ use_count, success_count
131
+ ) VALUES (
132
+ @memory_id, @user_id, @namespace, @memory_type, @content, @raw_text,
133
+ @document_date, @learned_at, @source_id, @confidence, @salience,
134
+ 1, @keywords, @event_date_start, @event_date_end,
135
+ @invalidated_at, 'warm', 1.0, 0,
136
+ 0, 0
137
+ )`
138
+ );
139
+ stmt.run({
140
+ memory_id: p.memoryId,
141
+ user_id: p.userId,
142
+ namespace: p.namespace ?? null,
143
+ memory_type: p.memoryType,
144
+ content: p.content,
145
+ raw_text: p.rawText ?? null,
146
+ document_date: p.documentDate ?? null,
147
+ learned_at: p.learnedAt,
148
+ source_id: p.sourceId ?? null,
149
+ confidence: p.confidence ?? 1,
150
+ salience: p.salience ?? 1,
151
+ keywords: p.keywords ? JSON.stringify(p.keywords) : null,
152
+ event_date_start: p.eventDateStart ?? null,
153
+ event_date_end: p.eventDateEnd ?? null,
154
+ invalidated_at: p.invalidatedAt ?? null
155
+ });
156
+ }
157
+ function applyRelationCreated(db, op) {
158
+ const p = parsePayload(op.payload);
159
+ db.prepare(
160
+ `INSERT OR IGNORE INTO memory_relations (
161
+ src_memory_id, dst_memory_id, relation_type, created_at, reason
162
+ ) VALUES (?, ?, ?, ?, ?)`
163
+ ).run(p.srcMemoryId, p.dstMemoryId, p.relationType, p.createdAt, p.reason ?? null);
164
+ if (p.relationType === "updates") {
165
+ db.prepare("UPDATE memories SET is_latest = 0 WHERE memory_id = ?").run(
166
+ p.dstMemoryId
167
+ );
168
+ }
169
+ }
170
+ function applyMemoryInvalidated(db, op) {
171
+ const p = parsePayload(op.payload);
172
+ db.prepare(
173
+ "UPDATE memories SET invalidated_at = ? WHERE memory_id = ?"
174
+ ).run(p.invalidatedAt, p.memoryId);
175
+ }
176
+ function applyFeedbackGiven(db, op) {
177
+ const p = parsePayload(op.payload);
178
+ if (p.helpful) {
179
+ db.prepare(
180
+ "UPDATE memories SET use_count = use_count + 1, success_count = success_count + 1 WHERE memory_id = ?"
181
+ ).run(p.memoryId);
182
+ } else {
183
+ db.prepare(
184
+ "UPDATE memories SET use_count = use_count + 1 WHERE memory_id = ?"
185
+ ).run(p.memoryId);
186
+ }
187
+ }
188
+ function applyMemoryTierSet(db, op) {
189
+ const p = parsePayload(op.payload);
190
+ db.prepare("UPDATE memories SET tier = ? WHERE memory_id = ?").run(
191
+ p.tier,
192
+ p.memoryId
193
+ );
194
+ }
195
+ function applyInboxOps(db, ops) {
196
+ const applied = [];
197
+ const skipped = [];
198
+ const runAll = db.transaction((batch) => {
199
+ for (const op of batch) {
200
+ try {
201
+ switch (op.opType) {
202
+ case "memory_created":
203
+ applyMemoryCreated(db, op);
204
+ break;
205
+ case "relation_created":
206
+ applyRelationCreated(db, op);
207
+ break;
208
+ case "memory_invalidated":
209
+ applyMemoryInvalidated(db, op);
210
+ break;
211
+ case "feedback_given":
212
+ applyFeedbackGiven(db, op);
213
+ break;
214
+ case "memory_tier_set":
215
+ applyMemoryTierSet(db, op);
216
+ break;
217
+ default:
218
+ skipped.push({ opId: op.opId, reason: `unknown op type: ${op.opType}` });
219
+ continue;
220
+ }
221
+ applied.push(op.opId);
222
+ } catch (err) {
223
+ skipped.push({
224
+ opId: op.opId,
225
+ reason: err instanceof Error ? err.message : String(err)
226
+ });
227
+ }
228
+ }
229
+ });
230
+ runAll(ops);
231
+ return { applied, skipped };
232
+ }
233
+
115
234
  // src/sync-client.ts
116
235
  var SyncClient = class {
117
236
  db;
@@ -137,6 +256,7 @@ var SyncClient = class {
137
256
  return {
138
257
  enabled: true,
139
258
  serverUrl: this.config.serverUrl,
259
+ userId: this.config.userId,
140
260
  deviceId: this.config.deviceId,
141
261
  pendingOps: this.outbox.countPending(),
142
262
  lastPush: {
@@ -209,14 +329,33 @@ var SyncClient = class {
209
329
  throw new Error(`Pull failed: ${response.status} ${response.statusText}`);
210
330
  }
211
331
  const body = await response.json();
212
- const { ops, nextCursor, hasMore } = body.data;
332
+ const { ops, nextCursor } = body.data;
213
333
  if (ops.length > 0) {
214
334
  this.inbox.addOps(ops);
215
335
  }
336
+ const pending = this.inbox.getPending();
337
+ if (pending.length > 0) {
338
+ const result = applyInboxOps(this.db, pending);
339
+ if (result.applied.length > 0) {
340
+ this.inbox.markApplied(result.applied);
341
+ }
342
+ }
216
343
  this.setCursor(nextCursor);
217
344
  this.setState("last_pull_success_at", (/* @__PURE__ */ new Date()).toISOString());
218
345
  return ops.length;
219
346
  }
347
+ /** For tests / advanced callers: apply currently-pending inbox ops manually. */
348
+ applyPendingInbox() {
349
+ const pending = this.inbox.getPending();
350
+ if (pending.length === 0) {
351
+ return { applied: [], skipped: [] };
352
+ }
353
+ const result = applyInboxOps(this.db, pending);
354
+ if (result.applied.length > 0) {
355
+ this.inbox.markApplied(result.applied);
356
+ }
357
+ return result;
358
+ }
220
359
  getState(key) {
221
360
  const row = this.db.prepare("SELECT value FROM sync_state WHERE key = ?").get(key);
222
361
  return row?.value ?? null;
@@ -238,5 +377,6 @@ export {
238
377
  Inbox,
239
378
  Outbox,
240
379
  SyncClient,
380
+ applyInboxOps,
241
381
  ensureSyncSchema
242
382
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memrosetta/sync-client",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Local-first sync client for MemRosetta (outbox/inbox, push/pull)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",