@agentlip/kernel 0.1.0
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/migrations/0001_schema_v1.sql +156 -0
- package/migrations/0001_schema_v1_fts.sql +32 -0
- package/package.json +27 -0
- package/src/events.ts +365 -0
- package/src/index.ts +316 -0
- package/src/messageMutations.ts +523 -0
- package/src/queries.ts +418 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message mutation helpers for @agentlip/kernel
|
|
3
|
+
*
|
|
4
|
+
* Implements bd-16d.2.6 (edit + tombstone delete) and bd-16d.2.8 (retopic)
|
|
5
|
+
*
|
|
6
|
+
* All mutations:
|
|
7
|
+
* - Run in single DB transaction
|
|
8
|
+
* - Update state (messages row(s))
|
|
9
|
+
* - Increment messages.version
|
|
10
|
+
* - Emit corresponding events via insertEvent
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Database } from "bun:sqlite";
|
|
14
|
+
import { insertEvent } from "./events";
|
|
15
|
+
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
// Types
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface EditMessageOptions {
|
|
21
|
+
db: Database;
|
|
22
|
+
messageId: string;
|
|
23
|
+
newContentRaw: string;
|
|
24
|
+
expectedVersion?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface EditMessageResult {
|
|
28
|
+
messageId: string;
|
|
29
|
+
version: number;
|
|
30
|
+
eventId: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TombstoneDeleteOptions {
|
|
34
|
+
db: Database;
|
|
35
|
+
messageId: string;
|
|
36
|
+
actor: string;
|
|
37
|
+
expectedVersion?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TombstoneDeleteResult {
|
|
41
|
+
messageId: string;
|
|
42
|
+
version: number;
|
|
43
|
+
eventId: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type RetopicMode = "one" | "later" | "all";
|
|
47
|
+
|
|
48
|
+
export interface RetopicMessageOptions {
|
|
49
|
+
db: Database;
|
|
50
|
+
messageId: string;
|
|
51
|
+
toTopicId: string;
|
|
52
|
+
mode: RetopicMode;
|
|
53
|
+
expectedVersion?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RetopicMessageResult {
|
|
57
|
+
affectedCount: number;
|
|
58
|
+
affectedMessages: Array<{
|
|
59
|
+
messageId: string;
|
|
60
|
+
version: number;
|
|
61
|
+
eventId: number;
|
|
62
|
+
}>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
// Error Classes
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Error thrown when expectedVersion doesn't match current message version.
|
|
71
|
+
* Contains current_version for 409 response.
|
|
72
|
+
*/
|
|
73
|
+
export class VersionConflictError extends Error {
|
|
74
|
+
readonly code = "VERSION_CONFLICT";
|
|
75
|
+
readonly messageId: string;
|
|
76
|
+
readonly expectedVersion: number;
|
|
77
|
+
readonly currentVersion: number;
|
|
78
|
+
|
|
79
|
+
constructor(messageId: string, expectedVersion: number, currentVersion: number) {
|
|
80
|
+
super(
|
|
81
|
+
`Version conflict for message ${messageId}: expected ${expectedVersion}, current ${currentVersion}`
|
|
82
|
+
);
|
|
83
|
+
this.name = "VersionConflictError";
|
|
84
|
+
this.messageId = messageId;
|
|
85
|
+
this.expectedVersion = expectedVersion;
|
|
86
|
+
this.currentVersion = currentVersion;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Error thrown when message is not found.
|
|
92
|
+
*/
|
|
93
|
+
export class MessageNotFoundError extends Error {
|
|
94
|
+
readonly code = "NOT_FOUND";
|
|
95
|
+
readonly messageId: string;
|
|
96
|
+
|
|
97
|
+
constructor(messageId: string) {
|
|
98
|
+
super(`Message not found: ${messageId}`);
|
|
99
|
+
this.name = "MessageNotFoundError";
|
|
100
|
+
this.messageId = messageId;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Error thrown when attempting cross-channel retopic.
|
|
106
|
+
*/
|
|
107
|
+
export class CrossChannelMoveError extends Error {
|
|
108
|
+
readonly code = "CROSS_CHANNEL_MOVE";
|
|
109
|
+
readonly messageId: string;
|
|
110
|
+
readonly sourceChannelId: string;
|
|
111
|
+
readonly targetChannelId: string;
|
|
112
|
+
|
|
113
|
+
constructor(messageId: string, sourceChannelId: string, targetChannelId: string) {
|
|
114
|
+
super(
|
|
115
|
+
`Cross-channel move forbidden: message ${messageId} is in channel ${sourceChannelId}, target topic is in channel ${targetChannelId}`
|
|
116
|
+
);
|
|
117
|
+
this.name = "CrossChannelMoveError";
|
|
118
|
+
this.messageId = messageId;
|
|
119
|
+
this.sourceChannelId = sourceChannelId;
|
|
120
|
+
this.targetChannelId = targetChannelId;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Error thrown when target topic is not found.
|
|
126
|
+
*/
|
|
127
|
+
export class TopicNotFoundError extends Error {
|
|
128
|
+
readonly code = "NOT_FOUND";
|
|
129
|
+
readonly topicId: string;
|
|
130
|
+
|
|
131
|
+
constructor(topicId: string) {
|
|
132
|
+
super(`Topic not found: ${topicId}`);
|
|
133
|
+
this.name = "TopicNotFoundError";
|
|
134
|
+
this.topicId = topicId;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
139
|
+
// Internal helpers
|
|
140
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
interface MessageRow {
|
|
143
|
+
id: string;
|
|
144
|
+
topic_id: string;
|
|
145
|
+
channel_id: string;
|
|
146
|
+
sender: string;
|
|
147
|
+
content_raw: string;
|
|
148
|
+
version: number;
|
|
149
|
+
created_at: string;
|
|
150
|
+
edited_at: string | null;
|
|
151
|
+
deleted_at: string | null;
|
|
152
|
+
deleted_by: string | null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface TopicRow {
|
|
156
|
+
id: string;
|
|
157
|
+
channel_id: string;
|
|
158
|
+
title: string;
|
|
159
|
+
created_at: string;
|
|
160
|
+
updated_at: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getMessageOrThrow(db: Database, messageId: string): MessageRow {
|
|
164
|
+
const message = db
|
|
165
|
+
.query<MessageRow, [string]>(`
|
|
166
|
+
SELECT id, topic_id, channel_id, sender, content_raw, version,
|
|
167
|
+
created_at, edited_at, deleted_at, deleted_by
|
|
168
|
+
FROM messages
|
|
169
|
+
WHERE id = ?
|
|
170
|
+
`)
|
|
171
|
+
.get(messageId);
|
|
172
|
+
|
|
173
|
+
if (!message) {
|
|
174
|
+
throw new MessageNotFoundError(messageId);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return message;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getTopicOrThrow(db: Database, topicId: string): TopicRow {
|
|
181
|
+
const topic = db
|
|
182
|
+
.query<TopicRow, [string]>(`
|
|
183
|
+
SELECT id, channel_id, title, created_at, updated_at
|
|
184
|
+
FROM topics
|
|
185
|
+
WHERE id = ?
|
|
186
|
+
`)
|
|
187
|
+
.get(topicId);
|
|
188
|
+
|
|
189
|
+
if (!topic) {
|
|
190
|
+
throw new TopicNotFoundError(topicId);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return topic;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function checkVersion(
|
|
197
|
+
message: MessageRow,
|
|
198
|
+
expectedVersion: number | undefined
|
|
199
|
+
): void {
|
|
200
|
+
if (expectedVersion !== undefined && message.version !== expectedVersion) {
|
|
201
|
+
throw new VersionConflictError(message.id, expectedVersion, message.version);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
206
|
+
// Edit Message
|
|
207
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Edit message content.
|
|
211
|
+
*
|
|
212
|
+
* Updates:
|
|
213
|
+
* - content_raw to newContentRaw
|
|
214
|
+
* - edited_at to now
|
|
215
|
+
* - version incremented by 1
|
|
216
|
+
*
|
|
217
|
+
* Emits: message.edited event with old_content, new_content, version
|
|
218
|
+
*
|
|
219
|
+
* @throws MessageNotFoundError if message doesn't exist
|
|
220
|
+
* @throws VersionConflictError if expectedVersion provided and mismatched
|
|
221
|
+
*/
|
|
222
|
+
export function editMessage(options: EditMessageOptions): EditMessageResult {
|
|
223
|
+
const { db, messageId, newContentRaw, expectedVersion } = options;
|
|
224
|
+
|
|
225
|
+
// Validate content length (per schema: max 64KB)
|
|
226
|
+
if (newContentRaw.length > 65536) {
|
|
227
|
+
throw new Error("Content too large: max 64KB");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Use transaction for atomicity
|
|
231
|
+
const result = db.transaction(() => {
|
|
232
|
+
// 1. Get and validate message
|
|
233
|
+
const message = getMessageOrThrow(db, messageId);
|
|
234
|
+
|
|
235
|
+
// 2. Check version if provided
|
|
236
|
+
checkVersion(message, expectedVersion);
|
|
237
|
+
|
|
238
|
+
const oldContent = message.content_raw;
|
|
239
|
+
const newVersion = message.version + 1;
|
|
240
|
+
const now = new Date().toISOString();
|
|
241
|
+
|
|
242
|
+
// 3. Update message state
|
|
243
|
+
db.run(
|
|
244
|
+
`UPDATE messages
|
|
245
|
+
SET content_raw = ?, edited_at = ?, version = ?
|
|
246
|
+
WHERE id = ?`,
|
|
247
|
+
[newContentRaw, now, newVersion, messageId]
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// 4. Emit event
|
|
251
|
+
const eventId = insertEvent({
|
|
252
|
+
db,
|
|
253
|
+
name: "message.edited",
|
|
254
|
+
scopes: {
|
|
255
|
+
channel_id: message.channel_id,
|
|
256
|
+
topic_id: message.topic_id,
|
|
257
|
+
},
|
|
258
|
+
entity: { type: "message", id: messageId },
|
|
259
|
+
data: {
|
|
260
|
+
message_id: messageId,
|
|
261
|
+
old_content: oldContent,
|
|
262
|
+
new_content: newContentRaw,
|
|
263
|
+
version: newVersion,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return { messageId, version: newVersion, eventId };
|
|
268
|
+
})();
|
|
269
|
+
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
274
|
+
// Tombstone Delete Message
|
|
275
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
const TOMBSTONE_CONTENT = "[deleted]";
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Tombstone delete a message.
|
|
281
|
+
*
|
|
282
|
+
* Updates:
|
|
283
|
+
* - deleted_at to now
|
|
284
|
+
* - deleted_by to actor
|
|
285
|
+
* - content_raw to "[deleted]"
|
|
286
|
+
* - edited_at to now
|
|
287
|
+
* - version incremented by 1
|
|
288
|
+
*
|
|
289
|
+
* Emits: message.deleted event with message_id, deleted_by, version
|
|
290
|
+
*
|
|
291
|
+
* Idempotent: if already deleted, returns success with no new event.
|
|
292
|
+
*
|
|
293
|
+
* @throws MessageNotFoundError if message doesn't exist
|
|
294
|
+
* @throws VersionConflictError if expectedVersion provided and mismatched
|
|
295
|
+
*/
|
|
296
|
+
export function tombstoneDeleteMessage(
|
|
297
|
+
options: TombstoneDeleteOptions
|
|
298
|
+
): TombstoneDeleteResult {
|
|
299
|
+
const { db, messageId, actor, expectedVersion } = options;
|
|
300
|
+
|
|
301
|
+
if (!actor || actor.trim().length === 0) {
|
|
302
|
+
throw new Error("Actor must be a non-empty string");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const result = db.transaction(() => {
|
|
306
|
+
// 1. Get and validate message
|
|
307
|
+
const message = getMessageOrThrow(db, messageId);
|
|
308
|
+
|
|
309
|
+
// 2. Check idempotency: if already deleted, return success
|
|
310
|
+
if (message.deleted_at !== null) {
|
|
311
|
+
// Return current state without new event (idempotent)
|
|
312
|
+
return {
|
|
313
|
+
messageId,
|
|
314
|
+
version: message.version,
|
|
315
|
+
eventId: 0, // No new event
|
|
316
|
+
alreadyDeleted: true,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 3. Check version if provided
|
|
321
|
+
checkVersion(message, expectedVersion);
|
|
322
|
+
|
|
323
|
+
const newVersion = message.version + 1;
|
|
324
|
+
const now = new Date().toISOString();
|
|
325
|
+
|
|
326
|
+
// 4. Update message state (tombstone)
|
|
327
|
+
db.run(
|
|
328
|
+
`UPDATE messages
|
|
329
|
+
SET deleted_at = ?, deleted_by = ?, content_raw = ?, edited_at = ?, version = ?
|
|
330
|
+
WHERE id = ?`,
|
|
331
|
+
[now, actor, TOMBSTONE_CONTENT, now, newVersion, messageId]
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// 5. Emit event
|
|
335
|
+
const eventId = insertEvent({
|
|
336
|
+
db,
|
|
337
|
+
name: "message.deleted",
|
|
338
|
+
scopes: {
|
|
339
|
+
channel_id: message.channel_id,
|
|
340
|
+
topic_id: message.topic_id,
|
|
341
|
+
},
|
|
342
|
+
entity: { type: "message", id: messageId },
|
|
343
|
+
data: {
|
|
344
|
+
message_id: messageId,
|
|
345
|
+
deleted_by: actor,
|
|
346
|
+
version: newVersion,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return { messageId, version: newVersion, eventId, alreadyDeleted: false };
|
|
351
|
+
})();
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
messageId: result.messageId,
|
|
355
|
+
version: result.version,
|
|
356
|
+
eventId: result.eventId,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
361
|
+
// Retopic Message
|
|
362
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Move message(s) to a different topic (same channel only).
|
|
366
|
+
*
|
|
367
|
+
* Modes:
|
|
368
|
+
* - "one": only the specified message
|
|
369
|
+
* - "later": message and all subsequent messages in the topic (by id order)
|
|
370
|
+
* - "all": all messages in the topic (regardless of position)
|
|
371
|
+
*
|
|
372
|
+
* For each affected message:
|
|
373
|
+
* - Updates topic_id
|
|
374
|
+
* - Increments version
|
|
375
|
+
* - Emits message.moved_topic event
|
|
376
|
+
*
|
|
377
|
+
* Idempotent: if message(s) already in target topic, returns success with no changes.
|
|
378
|
+
*
|
|
379
|
+
* @throws MessageNotFoundError if anchor message doesn't exist
|
|
380
|
+
* @throws TopicNotFoundError if target topic doesn't exist
|
|
381
|
+
* @throws CrossChannelMoveError if target topic is in different channel
|
|
382
|
+
* @throws VersionConflictError if expectedVersion provided and mismatched (for anchor message only)
|
|
383
|
+
*/
|
|
384
|
+
export function retopicMessage(
|
|
385
|
+
options: RetopicMessageOptions
|
|
386
|
+
): RetopicMessageResult {
|
|
387
|
+
const { db, messageId, toTopicId, mode, expectedVersion } = options;
|
|
388
|
+
|
|
389
|
+
const result = db.transaction(() => {
|
|
390
|
+
// 1. Get and validate anchor message
|
|
391
|
+
const anchorMessage = getMessageOrThrow(db, messageId);
|
|
392
|
+
|
|
393
|
+
// 2. Idempotent: if already in target topic (mode=one), return success
|
|
394
|
+
if (mode === "one" && anchorMessage.topic_id === toTopicId) {
|
|
395
|
+
return { affectedCount: 0, affectedMessages: [] };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 3. Check version if provided (only for anchor message)
|
|
399
|
+
checkVersion(anchorMessage, expectedVersion);
|
|
400
|
+
|
|
401
|
+
// 4. Get and validate target topic
|
|
402
|
+
const targetTopic = getTopicOrThrow(db, toTopicId);
|
|
403
|
+
|
|
404
|
+
// 5. Enforce same-channel constraint
|
|
405
|
+
if (anchorMessage.channel_id !== targetTopic.channel_id) {
|
|
406
|
+
throw new CrossChannelMoveError(
|
|
407
|
+
messageId,
|
|
408
|
+
anchorMessage.channel_id,
|
|
409
|
+
targetTopic.channel_id
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const oldTopicId = anchorMessage.topic_id;
|
|
414
|
+
|
|
415
|
+
// Idempotent: if already in target topic (any mode), return
|
|
416
|
+
if (oldTopicId === toTopicId) {
|
|
417
|
+
return { affectedCount: 0, affectedMessages: [] };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 6. Determine affected messages based on mode
|
|
421
|
+
let affectedMessageIds: string[];
|
|
422
|
+
|
|
423
|
+
switch (mode) {
|
|
424
|
+
case "one":
|
|
425
|
+
affectedMessageIds = [messageId];
|
|
426
|
+
break;
|
|
427
|
+
|
|
428
|
+
case "later":
|
|
429
|
+
// Messages with id >= anchor id in the same topic (by id order)
|
|
430
|
+
affectedMessageIds = db
|
|
431
|
+
.query<{ id: string }, [string, string]>(
|
|
432
|
+
`SELECT id FROM messages
|
|
433
|
+
WHERE topic_id = ? AND id >= ?
|
|
434
|
+
ORDER BY id ASC`
|
|
435
|
+
)
|
|
436
|
+
.all(oldTopicId, messageId)
|
|
437
|
+
.map((row) => row.id);
|
|
438
|
+
break;
|
|
439
|
+
|
|
440
|
+
case "all":
|
|
441
|
+
// All messages in the topic
|
|
442
|
+
affectedMessageIds = db
|
|
443
|
+
.query<{ id: string }, [string]>(
|
|
444
|
+
`SELECT id FROM messages
|
|
445
|
+
WHERE topic_id = ?
|
|
446
|
+
ORDER BY id ASC`
|
|
447
|
+
)
|
|
448
|
+
.all(oldTopicId)
|
|
449
|
+
.map((row) => row.id);
|
|
450
|
+
break;
|
|
451
|
+
|
|
452
|
+
default:
|
|
453
|
+
throw new Error(`Invalid retopic mode: ${mode}`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (affectedMessageIds.length === 0) {
|
|
457
|
+
return { affectedCount: 0, affectedMessages: [] };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const affectedMessages: Array<{
|
|
461
|
+
messageId: string;
|
|
462
|
+
version: number;
|
|
463
|
+
eventId: number;
|
|
464
|
+
}> = [];
|
|
465
|
+
|
|
466
|
+
const channelId = anchorMessage.channel_id;
|
|
467
|
+
|
|
468
|
+
// 7. For each affected message: update state + emit event
|
|
469
|
+
for (const affectedId of affectedMessageIds) {
|
|
470
|
+
// Get current version
|
|
471
|
+
const current = db
|
|
472
|
+
.query<{ version: number }, [string]>(
|
|
473
|
+
"SELECT version FROM messages WHERE id = ?"
|
|
474
|
+
)
|
|
475
|
+
.get(affectedId);
|
|
476
|
+
|
|
477
|
+
if (!current) continue; // Shouldn't happen, but be defensive
|
|
478
|
+
|
|
479
|
+
const newVersion = current.version + 1;
|
|
480
|
+
|
|
481
|
+
// Update topic_id and version
|
|
482
|
+
db.run(
|
|
483
|
+
`UPDATE messages
|
|
484
|
+
SET topic_id = ?, version = ?
|
|
485
|
+
WHERE id = ?`,
|
|
486
|
+
[toTopicId, newVersion, affectedId]
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
// Emit event
|
|
490
|
+
const eventId = insertEvent({
|
|
491
|
+
db,
|
|
492
|
+
name: "message.moved_topic",
|
|
493
|
+
scopes: {
|
|
494
|
+
channel_id: channelId,
|
|
495
|
+
topic_id: oldTopicId,
|
|
496
|
+
topic_id2: toTopicId,
|
|
497
|
+
},
|
|
498
|
+
entity: { type: "message", id: affectedId },
|
|
499
|
+
data: {
|
|
500
|
+
message_id: affectedId,
|
|
501
|
+
old_topic_id: oldTopicId,
|
|
502
|
+
new_topic_id: toTopicId,
|
|
503
|
+
channel_id: channelId,
|
|
504
|
+
mode,
|
|
505
|
+
version: newVersion,
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
affectedMessages.push({
|
|
510
|
+
messageId: affectedId,
|
|
511
|
+
version: newVersion,
|
|
512
|
+
eventId,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
affectedCount: affectedMessages.length,
|
|
518
|
+
affectedMessages,
|
|
519
|
+
};
|
|
520
|
+
})();
|
|
521
|
+
|
|
522
|
+
return result;
|
|
523
|
+
}
|