@memrosetta/sync-client 0.1.2 → 0.1.4
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 +34 -1
- package/dist/index.js +169 -3
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,37 @@ 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
|
+
* plus the same salience recompute rule used by core
|
|
36
|
+
* - `memory_tier_set` UPDATE of `tier`
|
|
37
|
+
*/
|
|
38
|
+
interface ApplyResult {
|
|
39
|
+
readonly applied: readonly string[];
|
|
40
|
+
readonly skipped: readonly {
|
|
41
|
+
readonly opId: string;
|
|
42
|
+
readonly reason: string;
|
|
43
|
+
}[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Apply a batch of pulled ops into the local memories/relations tables.
|
|
47
|
+
*
|
|
48
|
+
* The caller is responsible for passing only ops that have not been applied
|
|
49
|
+
* yet. `Inbox.getPending()` returns exactly that set.
|
|
50
|
+
*/
|
|
51
|
+
declare function applyInboxOps(db: Database.Database, ops: readonly SyncPulledOp[]): ApplyResult;
|
|
52
|
+
|
|
22
53
|
/**
|
|
23
54
|
* Minimal config required by SyncClient at runtime.
|
|
24
55
|
* serverUrl and apiKey are required (unlike the optional SyncConfig
|
|
@@ -61,6 +92,8 @@ declare class SyncClient {
|
|
|
61
92
|
getStatus(): SyncClientStatus;
|
|
62
93
|
push(): Promise<SyncClientPushResponse>;
|
|
63
94
|
pull(): Promise<number>;
|
|
95
|
+
/** For tests / advanced callers: apply currently-pending inbox ops manually. */
|
|
96
|
+
applyPendingInbox(): ApplyResult;
|
|
64
97
|
getState(key: string): string | null;
|
|
65
98
|
setState(key: string, value: string): void;
|
|
66
99
|
private getCursor;
|
|
@@ -69,4 +102,4 @@ declare class SyncClient {
|
|
|
69
102
|
|
|
70
103
|
declare function ensureSyncSchema(db: Database.Database): void;
|
|
71
104
|
|
|
72
|
-
export { Inbox, Outbox, SyncClient, type SyncClientConfig, type SyncClientPushResponse, type SyncClientStatus, type SyncStatusTimestamps, ensureSyncSchema };
|
|
105
|
+
export { type ApplyResult, Inbox, Outbox, SyncClient, type SyncClientConfig, type SyncClientPushResponse, type SyncClientStatus, type SyncStatusTimestamps, applyInboxOps, ensureSyncSchema };
|
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
|
}
|
|
@@ -112,6 +112,140 @@ 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 serializeKeywords(keywords) {
|
|
123
|
+
if (!keywords || keywords.length === 0) return null;
|
|
124
|
+
return keywords.join(" ");
|
|
125
|
+
}
|
|
126
|
+
function applyMemoryCreated(db, op) {
|
|
127
|
+
const p = parsePayload(op.payload);
|
|
128
|
+
const stmt = db.prepare(
|
|
129
|
+
`INSERT OR IGNORE INTO memories (
|
|
130
|
+
memory_id, user_id, namespace, memory_type, content, raw_text,
|
|
131
|
+
document_date, learned_at, source_id, confidence, salience,
|
|
132
|
+
is_latest, keywords, event_date_start, event_date_end,
|
|
133
|
+
invalidated_at, tier, activation_score, access_count,
|
|
134
|
+
use_count, success_count
|
|
135
|
+
) VALUES (
|
|
136
|
+
@memory_id, @user_id, @namespace, @memory_type, @content, @raw_text,
|
|
137
|
+
@document_date, @learned_at, @source_id, @confidence, @salience,
|
|
138
|
+
1, @keywords, @event_date_start, @event_date_end,
|
|
139
|
+
@invalidated_at, 'warm', 1.0, 0,
|
|
140
|
+
0, 0
|
|
141
|
+
)`
|
|
142
|
+
);
|
|
143
|
+
stmt.run({
|
|
144
|
+
memory_id: p.memoryId,
|
|
145
|
+
user_id: p.userId,
|
|
146
|
+
namespace: p.namespace ?? null,
|
|
147
|
+
memory_type: p.memoryType,
|
|
148
|
+
content: p.content,
|
|
149
|
+
raw_text: p.rawText ?? null,
|
|
150
|
+
document_date: p.documentDate ?? null,
|
|
151
|
+
learned_at: p.learnedAt,
|
|
152
|
+
source_id: p.sourceId ?? null,
|
|
153
|
+
confidence: p.confidence ?? 1,
|
|
154
|
+
salience: p.salience ?? 1,
|
|
155
|
+
keywords: serializeKeywords(p.keywords),
|
|
156
|
+
event_date_start: p.eventDateStart ?? null,
|
|
157
|
+
event_date_end: p.eventDateEnd ?? null,
|
|
158
|
+
invalidated_at: p.invalidatedAt ?? null
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function applyRelationCreated(db, op) {
|
|
162
|
+
const p = parsePayload(op.payload);
|
|
163
|
+
db.prepare(
|
|
164
|
+
`INSERT OR IGNORE INTO memory_relations (
|
|
165
|
+
src_memory_id, dst_memory_id, relation_type, created_at, reason
|
|
166
|
+
) VALUES (?, ?, ?, ?, ?)`
|
|
167
|
+
).run(p.srcMemoryId, p.dstMemoryId, p.relationType, p.createdAt, p.reason ?? null);
|
|
168
|
+
if (p.relationType === "updates") {
|
|
169
|
+
db.prepare("UPDATE memories SET is_latest = 0 WHERE memory_id = ?").run(
|
|
170
|
+
p.dstMemoryId
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function applyMemoryInvalidated(db, op) {
|
|
175
|
+
const p = parsePayload(op.payload);
|
|
176
|
+
db.prepare(
|
|
177
|
+
"UPDATE memories SET invalidated_at = ? WHERE memory_id = ?"
|
|
178
|
+
).run(p.invalidatedAt, p.memoryId);
|
|
179
|
+
}
|
|
180
|
+
function applyFeedbackGiven(db, op) {
|
|
181
|
+
const p = parsePayload(op.payload);
|
|
182
|
+
if (p.helpful) {
|
|
183
|
+
db.prepare(
|
|
184
|
+
"UPDATE memories SET use_count = use_count + 1, success_count = success_count + 1 WHERE memory_id = ?"
|
|
185
|
+
).run(p.memoryId);
|
|
186
|
+
} else {
|
|
187
|
+
db.prepare(
|
|
188
|
+
"UPDATE memories SET use_count = use_count + 1 WHERE memory_id = ?"
|
|
189
|
+
).run(p.memoryId);
|
|
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
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function applyMemoryTierSet(db, op) {
|
|
204
|
+
const p = parsePayload(op.payload);
|
|
205
|
+
db.prepare("UPDATE memories SET tier = ? WHERE memory_id = ?").run(
|
|
206
|
+
p.tier,
|
|
207
|
+
p.memoryId
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
function applyInboxOps(db, ops) {
|
|
211
|
+
const applied = [];
|
|
212
|
+
const skipped = [];
|
|
213
|
+
const runAll = db.transaction((batch) => {
|
|
214
|
+
for (const op of batch) {
|
|
215
|
+
try {
|
|
216
|
+
switch (op.opType) {
|
|
217
|
+
case "memory_created":
|
|
218
|
+
applyMemoryCreated(db, op);
|
|
219
|
+
break;
|
|
220
|
+
case "relation_created":
|
|
221
|
+
applyRelationCreated(db, op);
|
|
222
|
+
break;
|
|
223
|
+
case "memory_invalidated":
|
|
224
|
+
applyMemoryInvalidated(db, op);
|
|
225
|
+
break;
|
|
226
|
+
case "feedback_given":
|
|
227
|
+
applyFeedbackGiven(db, op);
|
|
228
|
+
break;
|
|
229
|
+
case "memory_tier_set":
|
|
230
|
+
applyMemoryTierSet(db, op);
|
|
231
|
+
break;
|
|
232
|
+
default:
|
|
233
|
+
skipped.push({ opId: op.opId, reason: `unknown op type: ${op.opType}` });
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
applied.push(op.opId);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
skipped.push({
|
|
239
|
+
opId: op.opId,
|
|
240
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
runAll(ops);
|
|
246
|
+
return { applied, skipped };
|
|
247
|
+
}
|
|
248
|
+
|
|
115
249
|
// src/sync-client.ts
|
|
116
250
|
var SyncClient = class {
|
|
117
251
|
db;
|
|
@@ -210,14 +344,45 @@ var SyncClient = class {
|
|
|
210
344
|
throw new Error(`Pull failed: ${response.status} ${response.statusText}`);
|
|
211
345
|
}
|
|
212
346
|
const body = await response.json();
|
|
213
|
-
const { ops, nextCursor
|
|
347
|
+
const { ops, nextCursor } = body.data;
|
|
214
348
|
if (ops.length > 0) {
|
|
215
349
|
this.inbox.addOps(ops);
|
|
216
350
|
}
|
|
351
|
+
const pending = this.inbox.getPending();
|
|
352
|
+
let skippedCount = 0;
|
|
353
|
+
if (pending.length > 0) {
|
|
354
|
+
const result = applyInboxOps(this.db, pending);
|
|
355
|
+
if (result.applied.length > 0) {
|
|
356
|
+
this.inbox.markApplied(result.applied);
|
|
357
|
+
}
|
|
358
|
+
skippedCount = result.skipped.length;
|
|
359
|
+
if (skippedCount > 0) {
|
|
360
|
+
for (const s of result.skipped) {
|
|
361
|
+
process.stderr.write(
|
|
362
|
+
`[sync] apply skipped op ${s.opId}: ${s.reason}
|
|
363
|
+
`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
217
368
|
this.setCursor(nextCursor);
|
|
218
|
-
|
|
369
|
+
if (skippedCount === 0) {
|
|
370
|
+
this.setState("last_pull_success_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
371
|
+
}
|
|
219
372
|
return ops.length;
|
|
220
373
|
}
|
|
374
|
+
/** For tests / advanced callers: apply currently-pending inbox ops manually. */
|
|
375
|
+
applyPendingInbox() {
|
|
376
|
+
const pending = this.inbox.getPending();
|
|
377
|
+
if (pending.length === 0) {
|
|
378
|
+
return { applied: [], skipped: [] };
|
|
379
|
+
}
|
|
380
|
+
const result = applyInboxOps(this.db, pending);
|
|
381
|
+
if (result.applied.length > 0) {
|
|
382
|
+
this.inbox.markApplied(result.applied);
|
|
383
|
+
}
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
221
386
|
getState(key) {
|
|
222
387
|
const row = this.db.prepare("SELECT value FROM sync_state WHERE key = ?").get(key);
|
|
223
388
|
return row?.value ?? null;
|
|
@@ -239,5 +404,6 @@ export {
|
|
|
239
404
|
Inbox,
|
|
240
405
|
Outbox,
|
|
241
406
|
SyncClient,
|
|
407
|
+
applyInboxOps,
|
|
242
408
|
ensureSyncSchema
|
|
243
409
|
};
|