mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
fix(import): harden desktop shared imports
Use the long-running worker timeout and refresh Desktop caches after successful push imports. Align auto-created member metadata and counts across initial and incremental imports, exclude SYSTEM from member updates, and keep existing-session writes validated and atomic.
This commit is contained in:
1 parent
5b35cb1d71
commit
db26950d4b
7 files changed
+458
-32
No files matched your search
@@ -222,6 +222,40 @@ test('deduplicates initial push batch when a no-platform-id message is repeated
|
||||
}
|
||||
})
|
||||
|
||||
test('counts explicit and auto-created members in initial push import results', async (t) => {
|
||||
const tempDir = makeTempDir()
|
||||
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
|
||||
|
||||
const manager = createDatabaseManager(tempDir)
|
||||
const outcome = await pushImport(manager, 'initial-auto-member-count', {
|
||||
chatlab: { version: '0.0.2', exportedAt: 1780330900 },
|
||||
meta: { name: 'Initial Auto Member Count', platform: 'wechat', type: 'group' },
|
||||
members: [{ platformId: 'wxid_alice', accountName: 'Alice' }],
|
||||
messages: [
|
||||
{ sender: 'wxid_alice', timestamp: 1780330832, type: 0, content: 'hello' },
|
||||
{ sender: 'wxid_bob', accountName: 'Bob', timestamp: 1780330833, type: 0, content: 'hi' },
|
||||
{ sender: 'SYSTEM', timestamp: 1780330834, type: 80, content: 'Bob joined the group' },
|
||||
],
|
||||
})
|
||||
|
||||
assert.equal(outcome.ok, true)
|
||||
if (!outcome.ok) return
|
||||
assert.equal(outcome.result.session.memberCount, 2)
|
||||
assert.deepEqual(outcome.result.updates, {
|
||||
metaUpdated: true,
|
||||
membersAdded: 2,
|
||||
membersUpdated: 0,
|
||||
})
|
||||
|
||||
const db = manager.openRawSessionDatabase('initial-auto-member-count', { readonly: true })
|
||||
try {
|
||||
const row = db.prepare('SELECT COUNT(*) as count FROM member').get() as { count: number }
|
||||
assert.equal(row.count, 3)
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('deduplicates incremental push when a later platform-id message matches an existing content hash', async (t) => {
|
||||
const tempDir = makeTempDir()
|
||||
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
|
||||
@@ -330,6 +364,55 @@ test('keeps distinct platform messages even when timestamp, sender and content m
|
||||
}
|
||||
})
|
||||
|
||||
test('preserves metadata and counts senders auto-created by incremental push imports', async (t) => {
|
||||
const tempDir = makeTempDir()
|
||||
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
|
||||
|
||||
const manager = createDatabaseManager(tempDir)
|
||||
const initialOutcome = await pushImport(manager, 'incremental-auto-member', {
|
||||
chatlab: { version: '0.0.2', exportedAt: 1780330900 },
|
||||
meta: { name: 'Incremental Auto Member', platform: 'wechat', type: 'group' },
|
||||
members: [{ platformId: 'wxid_alice', accountName: 'Alice' }],
|
||||
messages: [{ sender: 'wxid_alice', timestamp: 1780330832, type: 0, content: 'hello' }],
|
||||
})
|
||||
assert.equal(initialOutcome.ok, true)
|
||||
|
||||
const outcome = await pushImport(manager, 'incremental-auto-member', {
|
||||
messages: [
|
||||
{
|
||||
sender: 'wxid_bob',
|
||||
accountName: 'Bob',
|
||||
groupNickname: 'Product',
|
||||
timestamp: 1780330833,
|
||||
type: 0,
|
||||
content: 'hi',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
assert.equal(outcome.ok, true)
|
||||
if (!outcome.ok) return
|
||||
assert.deepEqual(outcome.result.updates, {
|
||||
metaUpdated: false,
|
||||
membersAdded: 1,
|
||||
membersUpdated: 0,
|
||||
})
|
||||
|
||||
const db = manager.openRawSessionDatabase('incremental-auto-member', { readonly: true })
|
||||
try {
|
||||
const member = db
|
||||
.prepare('SELECT platform_id, account_name, group_nickname FROM member WHERE platform_id = ?')
|
||||
.get('wxid_bob') as { platform_id: string; account_name: string; group_nickname: string }
|
||||
assert.deepEqual(member, {
|
||||
platform_id: 'wxid_bob',
|
||||
account_name: 'Bob',
|
||||
group_nickname: 'Product',
|
||||
})
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('creates push-import databases only inside the canonical databases directory', async (t) => {
|
||||
const tempDir = makeTempDir()
|
||||
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
|
||||
@@ -587,6 +670,144 @@ test('rejects non-string reply targets before applying incremental metadata upda
|
||||
}
|
||||
})
|
||||
|
||||
test('rejects malformed message metadata without partially applying incremental updates', async (t) => {
|
||||
const tempDir = makeTempDir()
|
||||
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
|
||||
|
||||
const manager = createDatabaseManager(tempDir)
|
||||
const initialOutcome = await pushImport(manager, 'invalid-message-metadata', {
|
||||
chatlab: { version: '0.0.2', exportedAt: 1780330900 },
|
||||
meta: { name: 'Original Session', platform: 'wechat', type: 'private' },
|
||||
members: [{ platformId: 'wxid_alice', accountName: 'Alice', groupNickname: 'Original Member' }],
|
||||
messages: [
|
||||
{
|
||||
sender: 'wxid_alice',
|
||||
accountName: 'Alice',
|
||||
timestamp: 1780330832,
|
||||
type: 0,
|
||||
content: 'hello',
|
||||
},
|
||||
],
|
||||
})
|
||||
assert.equal(initialOutcome.ok, true)
|
||||
|
||||
const malformedPayload = {
|
||||
meta: { name: 'Renamed Session', platform: 'wechat', type: 'private' },
|
||||
members: [{ platformId: 'wxid_alice', groupNickname: 'Updated Member' }],
|
||||
messages: [
|
||||
{
|
||||
sender: 'wxid_alice',
|
||||
accountName: { invalid: true },
|
||||
timestamp: 1780330833,
|
||||
type: 0,
|
||||
content: 'should fail',
|
||||
},
|
||||
],
|
||||
} as unknown as PushImportPayload
|
||||
|
||||
const outcome = await pushImport(manager, 'invalid-message-metadata', malformedPayload)
|
||||
const db = manager.openRawSessionDatabase('invalid-message-metadata', { readonly: true })
|
||||
try {
|
||||
const meta = db.prepare('SELECT name FROM meta').get() as { name: string }
|
||||
const member = db.prepare('SELECT group_nickname FROM member WHERE platform_id = ?').get('wxid_alice') as {
|
||||
group_nickname: string
|
||||
}
|
||||
const messages = db.prepare('SELECT COUNT(*) AS count FROM message').get() as { count: number }
|
||||
|
||||
assert.deepEqual(
|
||||
{
|
||||
reason: outcome.ok ? 'ok' : outcome.reason,
|
||||
metaName: meta.name,
|
||||
memberName: member.group_nickname,
|
||||
messageCount: messages.count,
|
||||
},
|
||||
{
|
||||
reason: 'invalid_payload',
|
||||
metaName: 'Original Session',
|
||||
memberName: 'Original Member',
|
||||
messageCount: 1,
|
||||
}
|
||||
)
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('rolls back metadata and member updates when incremental message writes fail', async (t) => {
|
||||
const tempDir = makeTempDir()
|
||||
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
|
||||
|
||||
const manager = createDatabaseManager(tempDir)
|
||||
const initialOutcome = await pushImport(manager, 'atomic-message-write', {
|
||||
chatlab: { version: '0.0.2', exportedAt: 1780330900 },
|
||||
meta: { name: 'Original Session', platform: 'wechat', type: 'private' },
|
||||
members: [{ platformId: 'wxid_alice', accountName: 'Alice', groupNickname: 'Original Member' }],
|
||||
messages: [
|
||||
{
|
||||
sender: 'wxid_alice',
|
||||
accountName: 'Alice',
|
||||
timestamp: 1780330832,
|
||||
type: 0,
|
||||
content: 'hello',
|
||||
},
|
||||
],
|
||||
})
|
||||
assert.equal(initialOutcome.ok, true)
|
||||
|
||||
const writableDb = manager.openRawSessionDatabase('atomic-message-write', { readonly: false })
|
||||
try {
|
||||
writableDb.exec(`
|
||||
CREATE TRIGGER fail_incremental_message_insert
|
||||
BEFORE INSERT ON message
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'forced message write failure');
|
||||
END
|
||||
`)
|
||||
} finally {
|
||||
writableDb.close()
|
||||
}
|
||||
|
||||
const outcome = await pushImport(manager, 'atomic-message-write', {
|
||||
meta: { name: 'Renamed Session', platform: 'wechat', type: 'private' },
|
||||
members: [{ platformId: 'wxid_alice', groupNickname: 'Updated Member' }],
|
||||
messages: [
|
||||
{
|
||||
sender: 'wxid_alice',
|
||||
accountName: 'Alice',
|
||||
timestamp: 1780330833,
|
||||
type: 0,
|
||||
content: 'should fail',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const db = manager.openRawSessionDatabase('atomic-message-write', { readonly: true })
|
||||
try {
|
||||
const meta = db.prepare('SELECT name FROM meta').get() as { name: string }
|
||||
const member = db.prepare('SELECT group_nickname FROM member WHERE platform_id = ?').get('wxid_alice') as {
|
||||
group_nickname: string
|
||||
}
|
||||
const messages = db.prepare('SELECT COUNT(*) AS count FROM message').get() as { count: number }
|
||||
|
||||
assert.deepEqual(
|
||||
{
|
||||
reason: outcome.ok ? 'ok' : outcome.reason,
|
||||
metaName: meta.name,
|
||||
memberName: member.group_nickname,
|
||||
messageCount: messages.count,
|
||||
},
|
||||
{
|
||||
reason: 'import_failed',
|
||||
metaName: 'Original Session',
|
||||
memberName: 'Original Member',
|
||||
messageCount: 1,
|
||||
}
|
||||
)
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('treats auto-created SYSTEM sender as a system member during initial import', async (t) => {
|
||||
const tempDir = makeTempDir()
|
||||
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
|
||||
@@ -609,6 +830,7 @@ test('treats auto-created SYSTEM sender as a system member during initial import
|
||||
assert.equal(outcome.ok, true)
|
||||
if (!outcome.ok) return
|
||||
assert.equal(outcome.result.session.memberCount, 0)
|
||||
assert.equal(outcome.result.updates.membersAdded, 0)
|
||||
|
||||
const db = manager.openRawSessionDatabase('system-sender', { readonly: true })
|
||||
try {
|
||||
@@ -625,6 +847,44 @@ test('treats auto-created SYSTEM sender as a system member during initial import
|
||||
}
|
||||
})
|
||||
|
||||
test('excludes an auto-created SYSTEM sender from incremental member counts', async (t) => {
|
||||
const tempDir = makeTempDir()
|
||||
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
|
||||
|
||||
const manager = createDatabaseManager(tempDir)
|
||||
const initialOutcome = await pushImport(manager, 'incremental-system-count', {
|
||||
chatlab: { version: '0.0.2', exportedAt: 1780330900 },
|
||||
meta: { name: 'System Count', platform: 'wechat', type: 'group' },
|
||||
members: [{ platformId: 'wxid_alice', accountName: 'Alice' }],
|
||||
messages: [{ sender: 'wxid_alice', accountName: 'Alice', timestamp: 1780330832, type: 0, content: 'hello' }],
|
||||
})
|
||||
assert.equal(initialOutcome.ok, true)
|
||||
|
||||
const outcome = await pushImport(manager, 'incremental-system-count', {
|
||||
messages: [{ sender: 'SYSTEM', timestamp: 1780330833, type: 80, content: 'Bob joined the group' }],
|
||||
})
|
||||
|
||||
assert.equal(outcome.ok, true)
|
||||
if (!outcome.ok) return
|
||||
assert.equal(outcome.result.session.memberCount, 1)
|
||||
assert.deepEqual(outcome.result.updates, {
|
||||
metaUpdated: false,
|
||||
membersAdded: 0,
|
||||
membersUpdated: 0,
|
||||
})
|
||||
|
||||
const db = manager.openRawSessionDatabase('incremental-system-count', { readonly: true })
|
||||
try {
|
||||
const members = db.prepare('SELECT platform_id, account_name FROM member ORDER BY platform_id').all()
|
||||
assert.deepEqual(members, [
|
||||
{ platform_id: 'SYSTEM', account_name: '系统消息' },
|
||||
{ platform_id: 'wxid_alice', account_name: 'Alice' },
|
||||
])
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('keeps SYSTEM member canonical during incremental member upserts', async (t) => {
|
||||
const tempDir = makeTempDir()
|
||||
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
|
||||
@@ -662,6 +922,11 @@ test('keeps SYSTEM member canonical during incremental member upserts', async (t
|
||||
assert.equal(outcome.ok, true)
|
||||
if (!outcome.ok) return
|
||||
assert.equal(outcome.result.session.memberCount, 0)
|
||||
assert.deepEqual(outcome.result.updates, {
|
||||
metaUpdated: false,
|
||||
membersAdded: 0,
|
||||
membersUpdated: 0,
|
||||
})
|
||||
|
||||
const db = manager.openRawSessionDatabase('system-member-upsert', { readonly: true })
|
||||
try {
|
||||
|
||||
@@ -82,19 +82,48 @@ export interface PushImportExecutionDeps {
|
||||
deleteDatabase(sessionId: string): void
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function validateOptionalString(value: unknown, field: string): string | null {
|
||||
return value !== undefined && typeof value !== 'string' ? `${field} must be a string` : null
|
||||
}
|
||||
|
||||
function validatePayload(payload: PushImportPayload, isNew: boolean): string | null {
|
||||
if (!isRecord(payload)) return 'payload must be an object'
|
||||
|
||||
const messages = payload.messages
|
||||
if (!Array.isArray(messages) || messages.length === 0) return 'messages is required and must be a non-empty array'
|
||||
|
||||
if (payload.chatlab !== undefined) {
|
||||
if (!isRecord(payload.chatlab)) return 'chatlab must be an object'
|
||||
if (typeof payload.chatlab.version !== 'string' || payload.chatlab.version.length === 0)
|
||||
return 'chatlab.version must be a string'
|
||||
if (typeof payload.chatlab.exportedAt !== 'number' || !Number.isFinite(payload.chatlab.exportedAt))
|
||||
return 'chatlab.exportedAt must be a number'
|
||||
const generatorError = validateOptionalString(payload.chatlab.generator, 'chatlab.generator')
|
||||
if (generatorError) return generatorError
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
if (!payload.chatlab) return 'chatlab is required for new sessions'
|
||||
if (!payload.meta) return 'meta is required for new sessions'
|
||||
const m = payload.meta
|
||||
if (!m.name) return 'meta.name is required'
|
||||
if (!m.platform) return 'meta.platform is required'
|
||||
if (!m.type) return 'meta.type is required'
|
||||
}
|
||||
|
||||
if (payload.meta !== undefined) {
|
||||
if (!isRecord(payload.meta)) return 'meta must be an object'
|
||||
const metaFields = ['name', 'platform', 'type', 'groupId', 'groupAvatar', 'ownerId'] as const
|
||||
for (const field of metaFields) {
|
||||
const error = validateOptionalString(payload.meta[field], `meta.${field}`)
|
||||
if (error) return error
|
||||
}
|
||||
if (isNew && !payload.meta.name) return 'meta.name is required'
|
||||
if (isNew && !payload.meta.platform) return 'meta.platform is required'
|
||||
if (isNew && !payload.meta.type) return 'meta.type is required'
|
||||
}
|
||||
|
||||
if (payload.options !== undefined && !isRecord(payload.options)) return 'options must be an object'
|
||||
const { metaUpdateMode, memberUpdateMode } = payload.options ?? {}
|
||||
if (metaUpdateMode !== undefined && metaUpdateMode !== 'patch' && metaUpdateMode !== 'none') {
|
||||
return `options.metaUpdateMode must be 'patch' or 'none'`
|
||||
@@ -106,17 +135,36 @@ function validatePayload(payload: PushImportPayload, isNew: boolean): string | n
|
||||
if (payload.members !== undefined && !Array.isArray(payload.members)) return 'members must be an array'
|
||||
if (payload.members) {
|
||||
for (let i = 0; i < payload.members.length; i++) {
|
||||
if (typeof payload.members[i].platformId !== 'string' || payload.members[i].platformId.length === 0)
|
||||
const member = payload.members[i]
|
||||
if (!isRecord(member)) return `members[${i}] must be an object`
|
||||
if (typeof member.platformId !== 'string' || member.platformId.length === 0)
|
||||
return `members[${i}].platformId must be a string`
|
||||
for (const field of ['accountName', 'groupNickname', 'avatar'] as const) {
|
||||
const error = validateOptionalString(member[field], `members[${i}].${field}`)
|
||||
if (error) return error
|
||||
}
|
||||
if (member.roles !== undefined) {
|
||||
if (!Array.isArray(member.roles)) return `members[${i}].roles must be an array`
|
||||
for (let j = 0; j < member.roles.length; j++) {
|
||||
const role = member.roles[j]
|
||||
if (!isRecord(role) || typeof role.id !== 'string' || role.id.length === 0)
|
||||
return `members[${i}].roles[${j}].id must be a string`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i]
|
||||
if (!isRecord(msg)) return `messages[${i}] must be an object`
|
||||
if (typeof msg.sender !== 'string' || msg.sender.length === 0) return `messages[${i}].sender must be a string`
|
||||
if (typeof msg.timestamp !== 'number' || msg.timestamp <= 0)
|
||||
if (typeof msg.timestamp !== 'number' || !Number.isFinite(msg.timestamp) || msg.timestamp <= 0)
|
||||
return `messages[${i}].timestamp must be a positive number`
|
||||
if (typeof msg.type !== 'number') return `messages[${i}].type must be a number`
|
||||
if (typeof msg.type !== 'number' || !Number.isFinite(msg.type)) return `messages[${i}].type must be a number`
|
||||
for (const field of ['accountName', 'groupNickname'] as const) {
|
||||
const error = validateOptionalString(msg[field], `messages[${i}].${field}`)
|
||||
if (error) return error
|
||||
}
|
||||
if (msg.content !== undefined && msg.content !== null && typeof msg.content !== 'string')
|
||||
return `messages[${i}].content must be a string or null`
|
||||
if (msg.platformMessageId !== undefined && typeof msg.platformMessageId !== 'string')
|
||||
@@ -148,6 +196,10 @@ function normalizeSenderGroupNickname(sender: string, groupNickname: string | un
|
||||
return sender === SYSTEM_SENDER_ID ? SYSTEM_MEMBER_NAME : groupNickname
|
||||
}
|
||||
|
||||
function isCountedMember(platformId: string): boolean {
|
||||
return platformId !== SYSTEM_SENDER_ID
|
||||
}
|
||||
|
||||
function writeMessages(
|
||||
db: DatabaseAdapter,
|
||||
messages: PushImportMessage[],
|
||||
@@ -155,6 +207,7 @@ function writeMessages(
|
||||
): {
|
||||
writtenCount: number
|
||||
duplicateCount: number
|
||||
membersAdded: number
|
||||
minWrittenTs: number
|
||||
ftsEntries: Array<{ id: number; content: string | null }>
|
||||
} {
|
||||
@@ -163,11 +216,14 @@ function writeMessages(
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
const getMemberId = db.prepare('SELECT id FROM member WHERE platform_id = ?')
|
||||
const insertMinimalMember = db.prepare('INSERT OR IGNORE INTO member (platform_id, account_name) VALUES (?, ?)')
|
||||
const insertMinimalMember = db.prepare(
|
||||
'INSERT OR IGNORE INTO member (platform_id, account_name, group_nickname) VALUES (?, ?, ?)'
|
||||
)
|
||||
|
||||
const memberIdCache = new Map<string, number>()
|
||||
let writtenCount = 0
|
||||
let duplicateCount = 0
|
||||
let membersAdded = 0
|
||||
let minWrittenTs = Infinity
|
||||
const ftsEntries: Array<{ id: number; content: string | null }> = []
|
||||
|
||||
@@ -194,11 +250,16 @@ function writeMessages(
|
||||
|
||||
let memberId = memberIdCache.get(msg.sender)
|
||||
if (!memberId) {
|
||||
insertMinimalMember.run(msg.sender, normalizeSenderAccountName(msg.sender, msg.accountName) || null)
|
||||
const insertResult = insertMinimalMember.run(
|
||||
msg.sender,
|
||||
normalizeSenderAccountName(msg.sender, msg.accountName) || null,
|
||||
normalizeSenderGroupNickname(msg.sender, msg.groupNickname) || null
|
||||
)
|
||||
const row = getMemberId.get(msg.sender) as { id: number } | undefined
|
||||
if (row) {
|
||||
memberId = row.id
|
||||
memberIdCache.set(msg.sender, memberId)
|
||||
if (isCountedMember(msg.sender)) membersAdded += insertResult.changes
|
||||
}
|
||||
}
|
||||
if (!memberId) continue
|
||||
@@ -219,7 +280,7 @@ function writeMessages(
|
||||
}
|
||||
})
|
||||
|
||||
return { writtenCount, duplicateCount, minWrittenTs, ftsEntries }
|
||||
return { writtenCount, duplicateCount, membersAdded, minWrittenTs, ftsEntries }
|
||||
}
|
||||
|
||||
function fullImport(
|
||||
@@ -227,7 +288,7 @@ function fullImport(
|
||||
meta: PushImportMeta,
|
||||
members: PushImportMember[],
|
||||
messages: PushImportMessage[]
|
||||
): { writtenCount: number; duplicateCount: number; membersAdded: number } {
|
||||
): { writtenCount: number; duplicateCount: number } {
|
||||
db.exec(CHAT_DB_SCHEMA)
|
||||
|
||||
// Auto-create minimal member entries for senders not listed in members,
|
||||
@@ -300,19 +361,18 @@ function fullImport(
|
||||
/* non-fatal */
|
||||
}
|
||||
|
||||
return { writtenCount: stats.messageCount, duplicateCount, membersAdded: members.length }
|
||||
return { writtenCount: stats.messageCount, duplicateCount }
|
||||
}
|
||||
|
||||
function incrementalImport(
|
||||
db: DatabaseAdapter,
|
||||
payload: PushImportPayload
|
||||
): {
|
||||
interface IncrementalImportStats {
|
||||
writtenCount: number
|
||||
duplicateCount: number
|
||||
metaUpdated: boolean
|
||||
membersAdded: number
|
||||
membersUpdated: number
|
||||
} {
|
||||
}
|
||||
|
||||
function writeIncrementalImport(db: DatabaseAdapter, payload: PushImportPayload): IncrementalImportStats {
|
||||
const metaUpdateMode = payload.options?.metaUpdateMode ?? 'patch'
|
||||
const memberUpdateMode = payload.options?.memberUpdateMode ?? 'upsert'
|
||||
|
||||
@@ -387,10 +447,10 @@ function incrementalImport(
|
||||
m.roles ? JSON.stringify(m.roles) : '[]'
|
||||
)
|
||||
if (!existed) {
|
||||
membersAdded++
|
||||
if (isCountedMember(m.platformId)) membersAdded++
|
||||
const row = getMemberId.get(m.platformId) as { id: number } | undefined
|
||||
if (row) existingMemberIds.add(m.platformId)
|
||||
} else {
|
||||
} else if (isCountedMember(m.platformId)) {
|
||||
membersUpdated++
|
||||
}
|
||||
}
|
||||
@@ -401,7 +461,13 @@ function incrementalImport(
|
||||
const preWriteMaxTs =
|
||||
(db.prepare('SELECT MAX(ts) as max_ts FROM message').get() as { max_ts: number | null })?.max_ts ?? 0
|
||||
|
||||
const { writtenCount, duplicateCount, minWrittenTs, ftsEntries } = writeMessages(db, payload.messages!, dedupState)
|
||||
const {
|
||||
writtenCount,
|
||||
duplicateCount,
|
||||
membersAdded: autoCreatedMembersAdded,
|
||||
minWrittenTs,
|
||||
ftsEntries,
|
||||
} = writeMessages(db, payload.messages!, dedupState)
|
||||
|
||||
if (ftsEntries.length > 0) {
|
||||
try {
|
||||
@@ -429,7 +495,17 @@ function incrementalImport(
|
||||
db.prepare('UPDATE meta SET imported_at = ?').run(Math.floor(Date.now() / 1000))
|
||||
}
|
||||
|
||||
return { writtenCount, duplicateCount, metaUpdated, membersAdded, membersUpdated }
|
||||
return {
|
||||
writtenCount,
|
||||
duplicateCount,
|
||||
metaUpdated,
|
||||
membersAdded: membersAdded + autoCreatedMembersAdded,
|
||||
membersUpdated,
|
||||
}
|
||||
}
|
||||
|
||||
function incrementalImport(db: DatabaseAdapter, payload: PushImportPayload): IncrementalImportStats {
|
||||
return db.transaction(() => writeIncrementalImport(db, payload))
|
||||
}
|
||||
|
||||
export async function pushImport(
|
||||
@@ -497,13 +573,11 @@ export async function executePushImportUnlocked(
|
||||
if (isNew) {
|
||||
const db = deps.openDatabase(sessionId, { create: true })
|
||||
try {
|
||||
const { writtenCount, duplicateCount, membersAdded } = fullImport(
|
||||
db,
|
||||
payload.meta!,
|
||||
payload.members ?? [],
|
||||
payload.messages!
|
||||
)
|
||||
const { writtenCount, duplicateCount } = fullImport(db, payload.meta!, payload.members ?? [], payload.messages!)
|
||||
const session = queryStats(db)
|
||||
// Every non-system member in a new database was inserted by this request,
|
||||
// including senders that were omitted from the submitted members array.
|
||||
const membersAdded = session.memberCount
|
||||
const outcome: PushImportOutcome = {
|
||||
ok: true,
|
||||
result: {
|
||||
|
||||
Reference in new issue
Block a user