@dexto/core 1.7.0 → 1.7.2

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.
@@ -33,7 +33,7 @@ class DatabaseHistoryProvider {
33
33
  logger;
34
34
  // Cache state
35
35
  cache = null;
36
- dirty = false;
36
+ pendingUpdates = /* @__PURE__ */ new Map();
37
37
  flushTimer = null;
38
38
  flushPromise = null;
39
39
  // Flush configuration
@@ -49,25 +49,25 @@ class DatabaseHistoryProvider {
49
49
  `DatabaseHistoryProvider: Session ${this.sessionId} hit message limit (${limit}), history may be truncated`
50
50
  );
51
51
  }
52
- const seen = /* @__PURE__ */ new Set();
52
+ const seenIndexes = /* @__PURE__ */ new Map();
53
53
  this.cache = [];
54
54
  let duplicateCount = 0;
55
55
  for (const msg of rawMessages) {
56
- if (msg.id && seen.has(msg.id)) {
56
+ const seenIndex = msg.id ? seenIndexes.get(msg.id) : void 0;
57
+ if (seenIndex !== void 0) {
57
58
  duplicateCount++;
59
+ this.cache[seenIndex] = msg;
58
60
  continue;
59
61
  }
60
62
  if (msg.id) {
61
- seen.add(msg.id);
63
+ seenIndexes.set(msg.id, this.cache.length);
62
64
  }
63
65
  this.cache.push(msg);
64
66
  }
65
67
  if (duplicateCount > 0) {
66
68
  this.logger.warn(
67
- `DatabaseHistoryProvider: Found ${duplicateCount} duplicate messages for session ${this.sessionId}, deduped to ${this.cache.length}`
69
+ `DatabaseHistoryProvider: Found ${duplicateCount} duplicate message updates for session ${this.sessionId}, deduped to ${this.cache.length}`
68
70
  );
69
- this.dirty = true;
70
- this.scheduleFlush();
71
71
  } else {
72
72
  this.logger.debug(
73
73
  `DatabaseHistoryProvider: Loaded ${this.cache.length} messages for session ${this.sessionId}`
@@ -125,10 +125,14 @@ class DatabaseHistoryProvider {
125
125
  if (this.cache === null) {
126
126
  await this.getHistory();
127
127
  }
128
- const index = this.cache.findIndex((m) => m.id === message.id);
128
+ const cache = this.cache;
129
+ if (cache === null) {
130
+ return;
131
+ }
132
+ const index = cache.findIndex((m) => m.id === message.id);
129
133
  if (index !== -1) {
130
- this.cache[index] = message;
131
- this.dirty = true;
134
+ cache[index] = message;
135
+ this.pendingUpdates.set(message.id, message);
132
136
  this.scheduleFlush();
133
137
  this.logger.debug(
134
138
  `DatabaseHistoryProvider: Updated message ${message.id} in cache for session ${this.sessionId}`
@@ -142,7 +146,7 @@ class DatabaseHistoryProvider {
142
146
  async clearHistory() {
143
147
  this.cancelPendingFlush();
144
148
  this.cache = [];
145
- this.dirty = false;
149
+ this.pendingUpdates.clear();
146
150
  const key = this.getMessagesKey();
147
151
  try {
148
152
  await this.database.delete(key);
@@ -159,67 +163,49 @@ class DatabaseHistoryProvider {
159
163
  );
160
164
  }
161
165
  }
162
- /**
163
- * Flush any pending updates to the database.
164
- * Should be called at turn boundaries to ensure durability.
165
- */
166
166
  async flush() {
167
167
  if (this.flushPromise) {
168
168
  await this.flushPromise;
169
- return;
170
169
  }
171
170
  this.cancelPendingFlush();
172
- if (!this.dirty || !this.cache) {
173
- return;
174
- }
175
- this.flushPromise = this.doFlush();
176
- try {
177
- await this.flushPromise;
178
- } finally {
179
- this.flushPromise = null;
171
+ while (this.pendingUpdates.size > 0) {
172
+ this.flushPromise = this.flushPendingUpdates();
173
+ try {
174
+ await this.flushPromise;
175
+ } finally {
176
+ this.flushPromise = null;
177
+ }
180
178
  }
181
179
  }
182
- /**
183
- * Internal flush implementation.
184
- * Writes entire cache to DB (delete + re-append all).
185
- */
186
- async doFlush() {
187
- if (!this.dirty || !this.cache) {
188
- return;
189
- }
180
+ async flushPendingUpdates() {
190
181
  const key = this.getMessagesKey();
191
- const snapshot = [...this.cache];
192
- const messageCount = snapshot.length;
182
+ const updates = [...this.pendingUpdates.values()];
183
+ this.pendingUpdates.clear();
193
184
  this.logger.debug(
194
- `DatabaseHistoryProvider: FLUSH START key=${key} snapshotSize=${messageCount} ids=[${snapshot.map((m) => m.id).join(",")}]`
185
+ `DatabaseHistoryProvider: FLUSH UPDATES key=${key} count=${updates.length} ids=[${updates.map((m) => m.id).join(",")}]`
195
186
  );
187
+ let failedIndex = updates.length;
196
188
  try {
197
- await this.database.delete(key);
198
- this.logger.debug(`DatabaseHistoryProvider: FLUSH DELETED key=${key}`);
199
- for (const msg of snapshot) {
200
- await this.database.append(key, msg);
201
- }
202
- this.logger.debug(
203
- `DatabaseHistoryProvider: FLUSH REAPPENDED key=${key} count=${messageCount}`
204
- );
205
- if (!this.flushTimer) {
206
- this.dirty = false;
189
+ for (const [index, message] of updates.entries()) {
190
+ failedIndex = index;
191
+ await this.database.append(key, message);
207
192
  }
208
193
  } catch (error) {
194
+ for (const message of updates.slice(failedIndex)) {
195
+ if (message.id && !this.pendingUpdates.has(message.id)) {
196
+ this.pendingUpdates.set(message.id, message);
197
+ }
198
+ }
209
199
  this.logger.error(
210
- `DatabaseHistoryProvider: Error flushing messages for session ${this.sessionId}: ${error instanceof Error ? error.message : String(error)}`
200
+ `DatabaseHistoryProvider: Error flushing message updates for session ${this.sessionId}: ${error instanceof Error ? error.message : String(error)}`
211
201
  );
212
202
  throw import_errors.SessionError.storageFailed(
213
203
  this.sessionId,
214
- "flush messages",
204
+ "flush message updates",
215
205
  error instanceof Error ? error.message : String(error)
216
206
  );
217
207
  }
218
208
  }
219
- /**
220
- * Schedule a debounced flush.
221
- * Batches rapid updateMessage() calls into a single DB write.
222
- */
223
209
  scheduleFlush() {
224
210
  if (this.flushTimer) {
225
211
  return;
@@ -230,9 +216,6 @@ class DatabaseHistoryProvider {
230
216
  });
231
217
  }, DatabaseHistoryProvider.FLUSH_DELAY_MS);
232
218
  }
233
- /**
234
- * Cancel any pending scheduled flush.
235
- */
236
219
  cancelPendingFlush() {
237
220
  if (this.flushTimer) {
238
221
  clearTimeout(this.flushTimer);
@@ -10,7 +10,7 @@ import type { ConversationHistoryProvider } from './types.js';
10
10
  * - getHistory(): Returns cached messages after first load (eliminates repeated DB reads)
11
11
  * - saveMessage(): Updates cache AND writes to DB immediately (new messages are critical)
12
12
  * - updateMessage(): Updates cache immediately, debounces DB writes (batches rapid updates)
13
- * - flush(): Forces all pending updates to DB (called at turn boundaries)
13
+ * - flush(): Appends the latest pending updates to DB (called at turn boundaries)
14
14
  * - clearHistory(): Clears cache and DB immediately
15
15
  *
16
16
  * Durability guarantees:
@@ -23,7 +23,7 @@ export declare class DatabaseHistoryProvider implements ConversationHistoryProvi
23
23
  private database;
24
24
  private logger;
25
25
  private cache;
26
- private dirty;
26
+ private pendingUpdates;
27
27
  private flushTimer;
28
28
  private flushPromise;
29
29
  private static readonly FLUSH_DELAY_MS;
@@ -32,24 +32,9 @@ export declare class DatabaseHistoryProvider implements ConversationHistoryProvi
32
32
  saveMessage(message: InternalMessage): Promise<void>;
33
33
  updateMessage(message: InternalMessage): Promise<void>;
34
34
  clearHistory(): Promise<void>;
35
- /**
36
- * Flush any pending updates to the database.
37
- * Should be called at turn boundaries to ensure durability.
38
- */
39
35
  flush(): Promise<void>;
40
- /**
41
- * Internal flush implementation.
42
- * Writes entire cache to DB (delete + re-append all).
43
- */
44
- private doFlush;
45
- /**
46
- * Schedule a debounced flush.
47
- * Batches rapid updateMessage() calls into a single DB write.
48
- */
36
+ private flushPendingUpdates;
49
37
  private scheduleFlush;
50
- /**
51
- * Cancel any pending scheduled flush.
52
- */
53
38
  private cancelPendingFlush;
54
39
  private getMessagesKey;
55
40
  }
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../src/session/history/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAE9D;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,uBAAwB,YAAW,2BAA2B;IAanE,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,QAAQ;IAbpB,OAAO,CAAC,MAAM,CAAS;IAGvB,OAAO,CAAC,KAAK,CAAkC;IAC/C,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,UAAU,CAA8C;IAChE,OAAO,CAAC,YAAY,CAA8B;IAGlD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAO;gBAGjC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAC1B,MAAM,EAAE,MAAM;IAKZ,UAAU,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IA2DxC,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAwCpD,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAiCtD,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BnC;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB5B;;;OAGG;YACW,OAAO;IA+CrB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAerB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,cAAc;CAGzB"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../src/session/history/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAE9D;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,uBAAwB,YAAW,2BAA2B;IAanE,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,QAAQ;IAbpB,OAAO,CAAC,MAAM,CAAS;IAGvB,OAAO,CAAC,KAAK,CAAkC;IAC/C,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,UAAU,CAA8C;IAChE,OAAO,CAAC,YAAY,CAA8B;IAGlD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAO;gBAGjC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAC1B,MAAM,EAAE,MAAM;IAKZ,UAAU,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAyDxC,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAwCpD,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAmCtD,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAkBd,mBAAmB;IAgCjC,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,cAAc;CAGzB"}
@@ -11,7 +11,7 @@ class DatabaseHistoryProvider {
11
11
  logger;
12
12
  // Cache state
13
13
  cache = null;
14
- dirty = false;
14
+ pendingUpdates = /* @__PURE__ */ new Map();
15
15
  flushTimer = null;
16
16
  flushPromise = null;
17
17
  // Flush configuration
@@ -27,25 +27,25 @@ class DatabaseHistoryProvider {
27
27
  `DatabaseHistoryProvider: Session ${this.sessionId} hit message limit (${limit}), history may be truncated`
28
28
  );
29
29
  }
30
- const seen = /* @__PURE__ */ new Set();
30
+ const seenIndexes = /* @__PURE__ */ new Map();
31
31
  this.cache = [];
32
32
  let duplicateCount = 0;
33
33
  for (const msg of rawMessages) {
34
- if (msg.id && seen.has(msg.id)) {
34
+ const seenIndex = msg.id ? seenIndexes.get(msg.id) : void 0;
35
+ if (seenIndex !== void 0) {
35
36
  duplicateCount++;
37
+ this.cache[seenIndex] = msg;
36
38
  continue;
37
39
  }
38
40
  if (msg.id) {
39
- seen.add(msg.id);
41
+ seenIndexes.set(msg.id, this.cache.length);
40
42
  }
41
43
  this.cache.push(msg);
42
44
  }
43
45
  if (duplicateCount > 0) {
44
46
  this.logger.warn(
45
- `DatabaseHistoryProvider: Found ${duplicateCount} duplicate messages for session ${this.sessionId}, deduped to ${this.cache.length}`
47
+ `DatabaseHistoryProvider: Found ${duplicateCount} duplicate message updates for session ${this.sessionId}, deduped to ${this.cache.length}`
46
48
  );
47
- this.dirty = true;
48
- this.scheduleFlush();
49
49
  } else {
50
50
  this.logger.debug(
51
51
  `DatabaseHistoryProvider: Loaded ${this.cache.length} messages for session ${this.sessionId}`
@@ -103,10 +103,14 @@ class DatabaseHistoryProvider {
103
103
  if (this.cache === null) {
104
104
  await this.getHistory();
105
105
  }
106
- const index = this.cache.findIndex((m) => m.id === message.id);
106
+ const cache = this.cache;
107
+ if (cache === null) {
108
+ return;
109
+ }
110
+ const index = cache.findIndex((m) => m.id === message.id);
107
111
  if (index !== -1) {
108
- this.cache[index] = message;
109
- this.dirty = true;
112
+ cache[index] = message;
113
+ this.pendingUpdates.set(message.id, message);
110
114
  this.scheduleFlush();
111
115
  this.logger.debug(
112
116
  `DatabaseHistoryProvider: Updated message ${message.id} in cache for session ${this.sessionId}`
@@ -120,7 +124,7 @@ class DatabaseHistoryProvider {
120
124
  async clearHistory() {
121
125
  this.cancelPendingFlush();
122
126
  this.cache = [];
123
- this.dirty = false;
127
+ this.pendingUpdates.clear();
124
128
  const key = this.getMessagesKey();
125
129
  try {
126
130
  await this.database.delete(key);
@@ -137,67 +141,49 @@ class DatabaseHistoryProvider {
137
141
  );
138
142
  }
139
143
  }
140
- /**
141
- * Flush any pending updates to the database.
142
- * Should be called at turn boundaries to ensure durability.
143
- */
144
144
  async flush() {
145
145
  if (this.flushPromise) {
146
146
  await this.flushPromise;
147
- return;
148
147
  }
149
148
  this.cancelPendingFlush();
150
- if (!this.dirty || !this.cache) {
151
- return;
152
- }
153
- this.flushPromise = this.doFlush();
154
- try {
155
- await this.flushPromise;
156
- } finally {
157
- this.flushPromise = null;
149
+ while (this.pendingUpdates.size > 0) {
150
+ this.flushPromise = this.flushPendingUpdates();
151
+ try {
152
+ await this.flushPromise;
153
+ } finally {
154
+ this.flushPromise = null;
155
+ }
158
156
  }
159
157
  }
160
- /**
161
- * Internal flush implementation.
162
- * Writes entire cache to DB (delete + re-append all).
163
- */
164
- async doFlush() {
165
- if (!this.dirty || !this.cache) {
166
- return;
167
- }
158
+ async flushPendingUpdates() {
168
159
  const key = this.getMessagesKey();
169
- const snapshot = [...this.cache];
170
- const messageCount = snapshot.length;
160
+ const updates = [...this.pendingUpdates.values()];
161
+ this.pendingUpdates.clear();
171
162
  this.logger.debug(
172
- `DatabaseHistoryProvider: FLUSH START key=${key} snapshotSize=${messageCount} ids=[${snapshot.map((m) => m.id).join(",")}]`
163
+ `DatabaseHistoryProvider: FLUSH UPDATES key=${key} count=${updates.length} ids=[${updates.map((m) => m.id).join(",")}]`
173
164
  );
165
+ let failedIndex = updates.length;
174
166
  try {
175
- await this.database.delete(key);
176
- this.logger.debug(`DatabaseHistoryProvider: FLUSH DELETED key=${key}`);
177
- for (const msg of snapshot) {
178
- await this.database.append(key, msg);
179
- }
180
- this.logger.debug(
181
- `DatabaseHistoryProvider: FLUSH REAPPENDED key=${key} count=${messageCount}`
182
- );
183
- if (!this.flushTimer) {
184
- this.dirty = false;
167
+ for (const [index, message] of updates.entries()) {
168
+ failedIndex = index;
169
+ await this.database.append(key, message);
185
170
  }
186
171
  } catch (error) {
172
+ for (const message of updates.slice(failedIndex)) {
173
+ if (message.id && !this.pendingUpdates.has(message.id)) {
174
+ this.pendingUpdates.set(message.id, message);
175
+ }
176
+ }
187
177
  this.logger.error(
188
- `DatabaseHistoryProvider: Error flushing messages for session ${this.sessionId}: ${error instanceof Error ? error.message : String(error)}`
178
+ `DatabaseHistoryProvider: Error flushing message updates for session ${this.sessionId}: ${error instanceof Error ? error.message : String(error)}`
189
179
  );
190
180
  throw SessionError.storageFailed(
191
181
  this.sessionId,
192
- "flush messages",
182
+ "flush message updates",
193
183
  error instanceof Error ? error.message : String(error)
194
184
  );
195
185
  }
196
186
  }
197
- /**
198
- * Schedule a debounced flush.
199
- * Batches rapid updateMessage() calls into a single DB write.
200
- */
201
187
  scheduleFlush() {
202
188
  if (this.flushTimer) {
203
189
  return;
@@ -208,9 +194,6 @@ class DatabaseHistoryProvider {
208
194
  });
209
195
  }, DatabaseHistoryProvider.FLUSH_DELAY_MS);
210
196
  }
211
- /**
212
- * Cancel any pending scheduled flush.
213
- */
214
197
  cancelPendingFlush() {
215
198
  if (this.flushTimer) {
216
199
  clearTimeout(this.flushTimer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dexto/core",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",