mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-04 22:06:06 +02:00
refactor activity tab
This commit is contained in:
@@ -1,219 +0,0 @@
|
||||
import { dbVars } from '../database';
|
||||
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const activityCache = {
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @returns {Promise<{
|
||||
* userId: string,
|
||||
* updatedAt: string,
|
||||
* isSelf: boolean,
|
||||
* sourceLastCreatedAt: string,
|
||||
* pendingSessionStartAt: number | null
|
||||
* } | null>}
|
||||
*/
|
||||
async getActivityCacheMeta(userId) {
|
||||
let row = null;
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
row = {
|
||||
userId: dbRow[0],
|
||||
updatedAt: dbRow[1],
|
||||
isSelf: Boolean(dbRow[2]),
|
||||
sourceLastCreatedAt: dbRow[3] || '',
|
||||
pendingSessionStartAt:
|
||||
typeof dbRow[4] === 'number' ? dbRow[4] : null
|
||||
};
|
||||
},
|
||||
`SELECT user_id, updated_at, is_self, source_last_created_at, pending_session_start_at
|
||||
FROM ${dbVars.userPrefix}_activity_cache_meta
|
||||
WHERE user_id = @userId`,
|
||||
{ '@userId': userId }
|
||||
);
|
||||
return row;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @returns {Promise<Array<{start: number, end: number}>>}
|
||||
*/
|
||||
async getActivityCacheSessions(userId) {
|
||||
const sessions = [];
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
sessions.push({
|
||||
start: dbRow[0],
|
||||
end: dbRow[1]
|
||||
});
|
||||
},
|
||||
`SELECT start_at, end_at
|
||||
FROM ${dbVars.userPrefix}_activity_cache_sessions
|
||||
WHERE user_id = @userId
|
||||
ORDER BY start_at`,
|
||||
{ '@userId': userId }
|
||||
);
|
||||
return sessions;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @returns {Promise<{
|
||||
* userId: string,
|
||||
* updatedAt: string,
|
||||
* isSelf: boolean,
|
||||
* sourceLastCreatedAt: string,
|
||||
* pendingSessionStartAt: number | null,
|
||||
* sessions: Array<{start: number, end: number}>
|
||||
* } | null>}
|
||||
*/
|
||||
async getActivityCache(userId) {
|
||||
const meta = await this.getActivityCacheMeta(userId);
|
||||
if (!meta) {
|
||||
return null;
|
||||
}
|
||||
const sessions = await this.getActivityCacheSessions(userId);
|
||||
return {
|
||||
...meta,
|
||||
sessions
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @returns {Promise<{start: number, end: number} | null>}
|
||||
*/
|
||||
async getLastActivityCacheSession(userId) {
|
||||
let row = null;
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
row = {
|
||||
start: dbRow[0],
|
||||
end: dbRow[1]
|
||||
};
|
||||
},
|
||||
`SELECT start_at, end_at
|
||||
FROM ${dbVars.userPrefix}_activity_cache_sessions
|
||||
WHERE user_id = @userId
|
||||
ORDER BY start_at DESC
|
||||
LIMIT 1`,
|
||||
{ '@userId': userId }
|
||||
);
|
||||
return row;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* userId: string,
|
||||
* updatedAt: string,
|
||||
* isSelf: boolean,
|
||||
* sourceLastCreatedAt: string,
|
||||
* pendingSessionStartAt: number | null,
|
||||
* sessions: Array<{start: number, end: number}>
|
||||
* }} entry
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async replaceActivityCache(entry) {
|
||||
await sqliteService.executeNonQuery('BEGIN');
|
||||
try {
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${dbVars.userPrefix}_activity_cache_sessions WHERE user_id = @userId`,
|
||||
{ '@userId': entry.userId }
|
||||
);
|
||||
await upsertSessions(entry.userId, entry.sessions);
|
||||
await upsertMeta(entry);
|
||||
await sqliteService.executeNonQuery('COMMIT');
|
||||
} catch (error) {
|
||||
await sqliteService.executeNonQuery('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* userId: string,
|
||||
* updatedAt: string,
|
||||
* isSelf: boolean,
|
||||
* sourceLastCreatedAt: string,
|
||||
* pendingSessionStartAt: number | null,
|
||||
* sessions: Array<{start: number, end: number}>,
|
||||
* replaceLastSession?: {start: number, end: number} | null
|
||||
* }} entry
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async appendActivityCache(entry) {
|
||||
await sqliteService.executeNonQuery('BEGIN');
|
||||
try {
|
||||
if (entry.replaceLastSession) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${dbVars.userPrefix}_activity_cache_sessions
|
||||
WHERE user_id = @userId AND start_at = @start AND end_at = @end`,
|
||||
{
|
||||
'@userId': entry.userId,
|
||||
'@start': entry.replaceLastSession.start,
|
||||
'@end': entry.replaceLastSession.end
|
||||
}
|
||||
);
|
||||
}
|
||||
await upsertSessions(entry.userId, entry.sessions);
|
||||
await upsertMeta(entry);
|
||||
await sqliteService.executeNonQuery('COMMIT');
|
||||
} catch (error) {
|
||||
await sqliteService.executeNonQuery('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* userId: string,
|
||||
* updatedAt: string,
|
||||
* isSelf: boolean,
|
||||
* sourceLastCreatedAt: string,
|
||||
* pendingSessionStartAt: number | null
|
||||
* }} entry
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async touchActivityCacheMeta(entry) {
|
||||
await upsertMeta(entry);
|
||||
}
|
||||
};
|
||||
|
||||
async function upsertMeta(entry) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${dbVars.userPrefix}_activity_cache_meta
|
||||
(user_id, updated_at, is_self, source_last_created_at, pending_session_start_at)
|
||||
VALUES (@user_id, @updated_at, @is_self, @source_last_created_at, @pending_session_start_at)`,
|
||||
{
|
||||
'@user_id': entry.userId,
|
||||
'@updated_at': entry.updatedAt,
|
||||
'@is_self': entry.isSelf ? 1 : 0,
|
||||
'@source_last_created_at': entry.sourceLastCreatedAt || '',
|
||||
'@pending_session_start_at': entry.pendingSessionStartAt
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function upsertSessions(userId, sessions = []) {
|
||||
const chunkSize = 250;
|
||||
for (let chunkStart = 0; chunkStart < sessions.length; chunkStart += chunkSize) {
|
||||
const chunk = sessions.slice(chunkStart, chunkStart + chunkSize);
|
||||
const args = {};
|
||||
const values = chunk.map((session, index) => {
|
||||
const suffix = `${chunkStart + index}`;
|
||||
args[`@user_id_${suffix}`] = userId;
|
||||
args[`@start_at_${suffix}`] = session.start;
|
||||
args[`@end_at_${suffix}`] = session.end;
|
||||
return `(@user_id_${suffix}, @start_at_${suffix}, @end_at_${suffix})`;
|
||||
});
|
||||
|
||||
await sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${dbVars.userPrefix}_activity_cache_sessions
|
||||
(user_id, start_at, end_at)
|
||||
VALUES ${values.join(', ')}`,
|
||||
args
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { activityCache };
|
||||
@@ -0,0 +1,504 @@
|
||||
import { dbVars } from '../database';
|
||||
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const ACTIVITY_VIEW_KIND = {
|
||||
ACTIVITY: 'activity',
|
||||
OVERLAP: 'overlap'
|
||||
};
|
||||
|
||||
const ACTIVITY_RANGE_CACHE_KIND = {
|
||||
SESSIONS: 0,
|
||||
TOP_WORLDS: 1
|
||||
};
|
||||
|
||||
function syncStateTable() {
|
||||
return `${dbVars.userPrefix}_activity_sync_state_v2`;
|
||||
}
|
||||
|
||||
function sessionsTable() {
|
||||
return `${dbVars.userPrefix}_activity_sessions_v2`;
|
||||
}
|
||||
|
||||
function rangeCacheTable() {
|
||||
return `${dbVars.userPrefix}_activity_range_cache_v2`;
|
||||
}
|
||||
|
||||
function bucketCacheTable() {
|
||||
return `${dbVars.userPrefix}_activity_bucket_cache_v2`;
|
||||
}
|
||||
|
||||
function topWorldsCacheTable() {
|
||||
return `${dbVars.userPrefix}_activity_top_worlds_cache_v2`;
|
||||
}
|
||||
|
||||
function parseJson(value, fallback) {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activity V2 is the formal, stable schema for the refactored Activity tab.
|
||||
* Legacy activity_cache_* tables remain only for upgrade compatibility.
|
||||
*/
|
||||
const activityV2 = {
|
||||
ACTIVITY_VIEW_KIND,
|
||||
ACTIVITY_RANGE_CACHE_KIND,
|
||||
|
||||
async getActivitySourceSliceV2({ userId, isSelf, fromDays, toDays = 0 }) {
|
||||
const fromDateIso = new Date(Date.now() - fromDays * 86400000).toISOString();
|
||||
const toDateIso = toDays > 0
|
||||
? new Date(Date.now() - toDays * 86400000).toISOString()
|
||||
: '';
|
||||
return isSelf
|
||||
? this.getCurrentUserLocationSliceV2(fromDateIso, toDateIso)
|
||||
: this.getFriendPresenceSliceV2(userId, fromDateIso, toDateIso);
|
||||
},
|
||||
|
||||
async getActivitySourceAfterV2({ userId, isSelf, afterCreatedAt, inclusive = false }) {
|
||||
return isSelf
|
||||
? this.getCurrentUserLocationAfterV2(afterCreatedAt, inclusive)
|
||||
: this.getFriendPresenceAfterV2(userId, afterCreatedAt);
|
||||
},
|
||||
|
||||
async getFriendPresenceSliceV2(userId, fromDateIso, toDateIso = '') {
|
||||
const rows = [];
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
rows.push({ created_at: dbRow[0], type: dbRow[1] });
|
||||
},
|
||||
`
|
||||
SELECT created_at, type
|
||||
FROM (
|
||||
SELECT created_at, type, 0 AS sort_group
|
||||
FROM (
|
||||
SELECT created_at, type
|
||||
FROM ${dbVars.userPrefix}_feed_online_offline
|
||||
WHERE user_id = @userId
|
||||
AND (type = 'Online' OR type = 'Offline')
|
||||
AND created_at < @fromDateIso
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
)
|
||||
UNION ALL
|
||||
SELECT created_at, type, 1 AS sort_group
|
||||
FROM ${dbVars.userPrefix}_feed_online_offline
|
||||
WHERE user_id = @userId
|
||||
AND (type = 'Online' OR type = 'Offline')
|
||||
AND created_at >= @fromDateIso
|
||||
${toDateIso ? 'AND created_at < @toDateIso' : ''}
|
||||
)
|
||||
ORDER BY created_at ASC, sort_group ASC
|
||||
`,
|
||||
{
|
||||
'@userId': userId,
|
||||
'@fromDateIso': fromDateIso,
|
||||
'@toDateIso': toDateIso
|
||||
}
|
||||
);
|
||||
|
||||
if (toDateIso) {
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
rows.push({ created_at: dbRow[0], type: dbRow[1] });
|
||||
},
|
||||
`SELECT created_at, type
|
||||
FROM ${dbVars.userPrefix}_feed_online_offline
|
||||
WHERE user_id = @userId
|
||||
AND (type = 'Online' OR type = 'Offline')
|
||||
AND created_at >= @toDateIso
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 1`,
|
||||
{
|
||||
'@userId': userId,
|
||||
'@toDateIso': toDateIso
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return rows.sort((left, right) => left.created_at.localeCompare(right.created_at));
|
||||
},
|
||||
|
||||
async getFriendPresenceAfterV2(userId, afterCreatedAt) {
|
||||
const rows = [];
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
rows.push({ created_at: dbRow[0], type: dbRow[1] });
|
||||
},
|
||||
`SELECT created_at, type
|
||||
FROM ${dbVars.userPrefix}_feed_online_offline
|
||||
WHERE user_id = @userId
|
||||
AND (type = 'Online' OR type = 'Offline')
|
||||
AND created_at > @afterCreatedAt
|
||||
ORDER BY created_at`,
|
||||
{
|
||||
'@userId': userId,
|
||||
'@afterCreatedAt': afterCreatedAt
|
||||
}
|
||||
);
|
||||
return rows;
|
||||
},
|
||||
|
||||
async getCurrentUserLocationSliceV2(fromDateIso, toDateIso = '') {
|
||||
const rows = [];
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
rows.push({ created_at: dbRow[0], time: dbRow[1] || 0 });
|
||||
},
|
||||
`
|
||||
SELECT created_at, time
|
||||
FROM (
|
||||
SELECT created_at, time, 0 AS sort_group
|
||||
FROM (
|
||||
SELECT created_at, time
|
||||
FROM gamelog_location
|
||||
WHERE created_at < @fromDateIso
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
)
|
||||
UNION ALL
|
||||
SELECT created_at, time, 1 AS sort_group
|
||||
FROM gamelog_location
|
||||
WHERE created_at >= @fromDateIso
|
||||
${toDateIso ? 'AND created_at < @toDateIso' : ''}
|
||||
${toDateIso
|
||||
? `UNION ALL
|
||||
SELECT created_at, time, 2 AS sort_group
|
||||
FROM (
|
||||
SELECT created_at, time
|
||||
FROM gamelog_location
|
||||
WHERE created_at >= @toDateIso
|
||||
ORDER BY created_at
|
||||
LIMIT 1
|
||||
)`
|
||||
: ''}
|
||||
)
|
||||
ORDER BY created_at ASC, sort_group ASC
|
||||
`,
|
||||
{
|
||||
'@fromDateIso': fromDateIso,
|
||||
'@toDateIso': toDateIso
|
||||
}
|
||||
);
|
||||
return rows;
|
||||
},
|
||||
|
||||
async getCurrentUserLocationAfterV2(afterCreatedAt, inclusive = false) {
|
||||
const rows = [];
|
||||
const operator = inclusive ? '>=' : '>';
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
rows.push({ created_at: dbRow[0], time: dbRow[1] || 0 });
|
||||
},
|
||||
`SELECT created_at, time
|
||||
FROM gamelog_location
|
||||
WHERE created_at ${operator} @afterCreatedAt
|
||||
ORDER BY created_at`,
|
||||
{ '@afterCreatedAt': afterCreatedAt }
|
||||
);
|
||||
return rows;
|
||||
},
|
||||
|
||||
async getActivitySyncStateV2(userId) {
|
||||
let row = null;
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
row = {
|
||||
userId: dbRow[0],
|
||||
updatedAt: dbRow[1] || '',
|
||||
isSelf: Boolean(dbRow[2]),
|
||||
sourceLastCreatedAt: dbRow[3] || '',
|
||||
pendingSessionStartAt: typeof dbRow[4] === 'number' ? dbRow[4] : null,
|
||||
cachedRangeDays: dbRow[5] || 0
|
||||
};
|
||||
},
|
||||
`SELECT user_id, updated_at, is_self, source_last_created_at, pending_session_start_at, cached_range_days
|
||||
FROM ${syncStateTable()}
|
||||
WHERE user_id = @userId`,
|
||||
{ '@userId': userId }
|
||||
);
|
||||
return row;
|
||||
},
|
||||
|
||||
async upsertActivitySyncStateV2(entry) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${syncStateTable()}
|
||||
(user_id, updated_at, is_self, source_last_created_at, pending_session_start_at, cached_range_days)
|
||||
VALUES (@userId, @updatedAt, @isSelf, @sourceLastCreatedAt, @pendingSessionStartAt, @cachedRangeDays)`,
|
||||
{
|
||||
'@userId': entry.userId,
|
||||
'@updatedAt': entry.updatedAt || '',
|
||||
'@isSelf': entry.isSelf ? 1 : 0,
|
||||
'@sourceLastCreatedAt': entry.sourceLastCreatedAt || '',
|
||||
'@pendingSessionStartAt': entry.pendingSessionStartAt,
|
||||
'@cachedRangeDays': entry.cachedRangeDays || 0
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async getActivitySessionsV2(userId) {
|
||||
const sessions = [];
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
sessions.push({
|
||||
start: dbRow[0],
|
||||
end: dbRow[1],
|
||||
isOpenTail: Boolean(dbRow[2]),
|
||||
sourceRevision: dbRow[3] || ''
|
||||
});
|
||||
},
|
||||
`SELECT start_at, end_at, is_open_tail, source_revision
|
||||
FROM ${sessionsTable()}
|
||||
WHERE user_id = @userId
|
||||
ORDER BY start_at`,
|
||||
{ '@userId': userId }
|
||||
);
|
||||
return sessions;
|
||||
},
|
||||
|
||||
async replaceActivitySessionsV2(userId, sessions = []) {
|
||||
await sqliteService.executeNonQuery('BEGIN');
|
||||
try {
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${sessionsTable()} WHERE user_id = @userId`,
|
||||
{ '@userId': userId }
|
||||
);
|
||||
await insertSessions(userId, sessions);
|
||||
await sqliteService.executeNonQuery('COMMIT');
|
||||
} catch (error) {
|
||||
await sqliteService.executeNonQuery('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async appendActivitySessionsV2({ userId, sessions = [], replaceFromStartAt = null }) {
|
||||
await sqliteService.executeNonQuery('BEGIN');
|
||||
try {
|
||||
if (replaceFromStartAt !== null) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${sessionsTable()}
|
||||
WHERE user_id = @userId AND start_at >= @replaceFromStartAt`,
|
||||
{
|
||||
'@userId': userId,
|
||||
'@replaceFromStartAt': replaceFromStartAt
|
||||
}
|
||||
);
|
||||
}
|
||||
await insertSessions(userId, sessions);
|
||||
await sqliteService.executeNonQuery('COMMIT');
|
||||
} catch (error) {
|
||||
await sqliteService.executeNonQuery('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getActivityRangeCacheV2(userId, rangeDays, cacheKind) {
|
||||
let row = null;
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
row = {
|
||||
userId: dbRow[0],
|
||||
rangeDays: dbRow[1],
|
||||
cacheKind: dbRow[2],
|
||||
isComplete: Boolean(dbRow[3]),
|
||||
builtFromCursor: dbRow[4] || '',
|
||||
builtAt: dbRow[5] || ''
|
||||
};
|
||||
},
|
||||
`SELECT user_id, range_days, cache_kind, is_complete, built_from_cursor, built_at
|
||||
FROM ${rangeCacheTable()}
|
||||
WHERE user_id = @userId AND range_days = @rangeDays AND cache_kind = @cacheKind`,
|
||||
{
|
||||
'@userId': userId,
|
||||
'@rangeDays': rangeDays,
|
||||
'@cacheKind': cacheKind
|
||||
}
|
||||
);
|
||||
return row;
|
||||
},
|
||||
|
||||
async upsertActivityRangeCacheV2(entry) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${rangeCacheTable()}
|
||||
(user_id, range_days, cache_kind, is_complete, built_from_cursor, built_at)
|
||||
VALUES (@userId, @rangeDays, @cacheKind, @isComplete, @builtFromCursor, @builtAt)`,
|
||||
{
|
||||
'@userId': entry.userId,
|
||||
'@rangeDays': entry.rangeDays,
|
||||
'@cacheKind': entry.cacheKind,
|
||||
'@isComplete': entry.isComplete ? 1 : 0,
|
||||
'@builtFromCursor': entry.builtFromCursor || '',
|
||||
'@builtAt': entry.builtAt || ''
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async getActivityBucketCacheV2({
|
||||
ownerUserId,
|
||||
targetUserId = '',
|
||||
rangeDays,
|
||||
viewKind,
|
||||
excludeKey = ''
|
||||
}) {
|
||||
let row = null;
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
row = {
|
||||
ownerUserId: dbRow[0],
|
||||
targetUserId: dbRow[1],
|
||||
rangeDays: dbRow[2],
|
||||
viewKind: dbRow[3],
|
||||
excludeKey: dbRow[4] || '',
|
||||
bucketVersion: dbRow[5] || 1,
|
||||
builtFromCursor: dbRow[6] || '',
|
||||
rawBuckets: parseJson(dbRow[7], []),
|
||||
normalizedBuckets: parseJson(dbRow[8], []),
|
||||
summary: parseJson(dbRow[9], {}),
|
||||
builtAt: dbRow[10] || ''
|
||||
};
|
||||
},
|
||||
`SELECT user_id, target_user_id, range_days, view_kind, exclude_key, bucket_version, built_from_cursor, raw_buckets_json, normalized_buckets_json, summary_json, built_at
|
||||
FROM ${bucketCacheTable()}
|
||||
WHERE user_id = @ownerUserId AND target_user_id = @targetUserId AND range_days = @rangeDays AND view_kind = @viewKind AND exclude_key = @excludeKey`,
|
||||
{
|
||||
'@ownerUserId': ownerUserId,
|
||||
'@targetUserId': targetUserId,
|
||||
'@rangeDays': rangeDays,
|
||||
'@viewKind': viewKind,
|
||||
'@excludeKey': excludeKey
|
||||
}
|
||||
);
|
||||
return row;
|
||||
},
|
||||
|
||||
async upsertActivityBucketCacheV2(entry) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${bucketCacheTable()}
|
||||
(user_id, target_user_id, range_days, view_kind, exclude_key, bucket_version, built_from_cursor, raw_buckets_json, normalized_buckets_json, summary_json, built_at)
|
||||
VALUES (@ownerUserId, @targetUserId, @rangeDays, @viewKind, @excludeKey, @bucketVersion, @builtFromCursor, @rawBucketsJson, @normalizedBucketsJson, @summaryJson, @builtAt)`,
|
||||
{
|
||||
'@ownerUserId': entry.ownerUserId,
|
||||
'@targetUserId': entry.targetUserId || '',
|
||||
'@rangeDays': entry.rangeDays,
|
||||
'@viewKind': entry.viewKind,
|
||||
'@excludeKey': entry.excludeKey || '',
|
||||
'@bucketVersion': entry.bucketVersion || 1,
|
||||
'@builtFromCursor': entry.builtFromCursor || '',
|
||||
'@rawBucketsJson': JSON.stringify(entry.rawBuckets || []),
|
||||
'@normalizedBucketsJson': JSON.stringify(entry.normalizedBuckets || []),
|
||||
'@summaryJson': JSON.stringify(entry.summary || {}),
|
||||
'@builtAt': entry.builtAt || ''
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async getActivityTopWorldsCacheV2(userId, rangeDays) {
|
||||
const worlds = [];
|
||||
let builtFromCursor = '';
|
||||
let builtAt = '';
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
builtFromCursor = dbRow[0] || builtFromCursor;
|
||||
builtAt = dbRow[1] || builtAt;
|
||||
worlds.push({
|
||||
worldId: dbRow[3],
|
||||
worldName: dbRow[4],
|
||||
visitCount: dbRow[5] || 0,
|
||||
totalTime: dbRow[6] || 0
|
||||
});
|
||||
},
|
||||
`SELECT built_from_cursor, built_at, rank_index, world_id, world_name, visit_count, total_time
|
||||
FROM ${topWorldsCacheTable()}
|
||||
WHERE user_id = @userId AND range_days = @rangeDays
|
||||
ORDER BY rank_index`,
|
||||
{
|
||||
'@userId': userId,
|
||||
'@rangeDays': rangeDays
|
||||
}
|
||||
);
|
||||
if (worlds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
userId,
|
||||
rangeDays,
|
||||
builtFromCursor,
|
||||
builtAt,
|
||||
worlds
|
||||
};
|
||||
},
|
||||
|
||||
async replaceActivityTopWorldsCacheV2(entry) {
|
||||
await sqliteService.executeNonQuery('BEGIN');
|
||||
try {
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${topWorldsCacheTable()} WHERE user_id = @userId AND range_days = @rangeDays`,
|
||||
{
|
||||
'@userId': entry.userId,
|
||||
'@rangeDays': entry.rangeDays
|
||||
}
|
||||
);
|
||||
|
||||
for (let index = 0; index < entry.worlds.length; index++) {
|
||||
const world = entry.worlds[index];
|
||||
await sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${topWorldsCacheTable()}
|
||||
(user_id, range_days, rank_index, world_id, world_name, visit_count, total_time, built_from_cursor, built_at)
|
||||
VALUES (@userId, @rangeDays, @rankIndex, @worldId, @worldName, @visitCount, @totalTime, @builtFromCursor, @builtAt)`,
|
||||
{
|
||||
'@userId': entry.userId,
|
||||
'@rangeDays': entry.rangeDays,
|
||||
'@rankIndex': index,
|
||||
'@worldId': world.worldId,
|
||||
'@worldName': world.worldName || world.worldId,
|
||||
'@visitCount': world.visitCount || 0,
|
||||
'@totalTime': world.totalTime || 0,
|
||||
'@builtFromCursor': entry.builtFromCursor || '',
|
||||
'@builtAt': entry.builtAt || ''
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await sqliteService.executeNonQuery('COMMIT');
|
||||
} catch (error) {
|
||||
await sqliteService.executeNonQuery('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function insertSessions(userId, sessions = []) {
|
||||
if (sessions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chunkSize = 250;
|
||||
for (let chunkStart = 0; chunkStart < sessions.length; chunkStart += chunkSize) {
|
||||
const chunk = sessions.slice(chunkStart, chunkStart + chunkSize);
|
||||
const args = {};
|
||||
const values = chunk.map((session, index) => {
|
||||
const suffix = `${chunkStart + index}`;
|
||||
args[`@userId_${suffix}`] = userId;
|
||||
args[`@startAt_${suffix}`] = session.start;
|
||||
args[`@endAt_${suffix}`] = session.end;
|
||||
args[`@isOpenTail_${suffix}`] = session.isOpenTail ? 1 : 0;
|
||||
args[`@sourceRevision_${suffix}`] = session.sourceRevision || '';
|
||||
return `(@userId_${suffix}, @startAt_${suffix}, @endAt_${suffix}, @isOpenTail_${suffix}, @sourceRevision_${suffix})`;
|
||||
});
|
||||
|
||||
await sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${sessionsTable()}
|
||||
(user_id, start_at, end_at, is_open_tail, source_revision)
|
||||
VALUES ${values.join(', ')}`,
|
||||
args
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { activityV2 };
|
||||
@@ -594,59 +594,6 @@ const feed = {
|
||||
return feedDatabase;
|
||||
},
|
||||
|
||||
async getOnlineFrequencyData(userId) {
|
||||
const data = [];
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
data.push(dbRow[0]);
|
||||
},
|
||||
`SELECT created_at FROM ${dbVars.userPrefix}_feed_online_offline WHERE type = 'Online' AND user_id = @userId ORDER BY created_at`,
|
||||
{ '@userId': userId }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Online and Offline events for a user to build sessions
|
||||
* @param {string} userId
|
||||
* @returns {Promise<Array<{created_at: string, type: string}>>}
|
||||
*/
|
||||
async getOnlineOfflineSessions(userId) {
|
||||
const data = [];
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
data.push({ created_at: dbRow[0], type: dbRow[1] });
|
||||
},
|
||||
`SELECT created_at, type FROM ${dbVars.userPrefix}_feed_online_offline WHERE user_id = @userId AND (type = 'Online' OR type = 'Offline') ORDER BY created_at`,
|
||||
{ '@userId': userId }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @param {string} afterCreatedAt
|
||||
* @returns {Promise<Array<{created_at: string, type: string}>>}
|
||||
*/
|
||||
async getOnlineOfflineSessionsAfter(userId, afterCreatedAt) {
|
||||
const data = [];
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
data.push({ created_at: dbRow[0], type: dbRow[1] });
|
||||
},
|
||||
`SELECT created_at, type FROM ${dbVars.userPrefix}_feed_online_offline
|
||||
WHERE user_id = @userId
|
||||
AND (type = 'Online' OR type = 'Offline')
|
||||
AND created_at > @afterCreatedAt
|
||||
ORDER BY created_at`,
|
||||
{
|
||||
'@userId': userId,
|
||||
'@afterCreatedAt': afterCreatedAt
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} days - Number of days to look back
|
||||
* @param {number} limit - Max number of worlds to return
|
||||
|
||||
@@ -348,9 +348,12 @@ const gameLog = {
|
||||
};
|
||||
data.set(row.location, row);
|
||||
},
|
||||
`SELECT created_at, location, time, world_name, group_name FROM gamelog_location WHERE location LIKE '%${groupId}%' ORDER BY id DESC`,
|
||||
`SELECT created_at, location, time, world_name, group_name
|
||||
FROM gamelog_location
|
||||
WHERE location LIKE @groupId
|
||||
ORDER BY id DESC`,
|
||||
{
|
||||
'@groupId': groupId
|
||||
'@groupId': `%${groupId}%`
|
||||
}
|
||||
);
|
||||
return data;
|
||||
@@ -828,7 +831,7 @@ const gameLog = {
|
||||
checkString = `AND resource_type != 'StringLoad'`;
|
||||
}
|
||||
if (!resourceload_image) {
|
||||
checkString = `AND resource_type != 'ImageLoad'`;
|
||||
checkImage = `AND resource_type != 'ImageLoad'`;
|
||||
}
|
||||
selects.push(
|
||||
`SELECT * FROM (SELECT id, created_at, resource_type AS type, NULL AS display_name, location, NULL AS user_id, NULL AS time, NULL AS world_id, NULL AS world_name, NULL AS group_name, NULL AS instance_id, NULL AS video_url, NULL AS video_name, NULL AS video_id, resource_url, resource_type, NULL AS data, NULL AS message FROM gamelog_resource_load WHERE 1=1 ${checkString} ${checkImage} ORDER BY id DESC LIMIT @perTable)`
|
||||
@@ -1048,7 +1051,7 @@ const gameLog = {
|
||||
checkString = `AND resource_type != 'StringLoad'`;
|
||||
}
|
||||
if (!resourceload_image) {
|
||||
checkString = `AND resource_type != 'ImageLoad'`;
|
||||
checkImage = `AND resource_type != 'ImageLoad'`;
|
||||
}
|
||||
selects.push(
|
||||
`SELECT * FROM (SELECT id, created_at, resource_type AS type, NULL AS display_name, location, NULL AS user_id, NULL AS time, NULL AS world_id, NULL AS world_name, NULL AS group_name, NULL AS instance_id, NULL AS video_url, NULL AS video_name, NULL AS video_id, resource_url, resource_type, NULL AS data, NULL AS message FROM gamelog_resource_load WHERE resource_url LIKE @searchLike ${checkString} ${checkImage} ORDER BY id DESC LIMIT @perTable)`
|
||||
@@ -1376,28 +1379,60 @@ const gameLog = {
|
||||
* Get current user's online sessions from gamelog_location
|
||||
* Each row has created_at (leave time) and time (duration in ms)
|
||||
* Session start = created_at - time, Session end = created_at
|
||||
* @param {number} [fromDays=0] - How many days back to start (0 = all time)
|
||||
* @param {number} [toDays=0] - How many days back to stop (0 = now)
|
||||
* @returns {Promise<Array<{created_at: string, time: number}>>}
|
||||
*/
|
||||
async getCurrentUserOnlineSessions() {
|
||||
async getCurrentUserOnlineSessions(fromDays = 0, toDays = 0) {
|
||||
const data = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
data.push({ created_at: dbRow[0], time: dbRow[1] || 0 });
|
||||
}, `SELECT created_at, time FROM gamelog_location ORDER BY created_at`);
|
||||
const now = new Date();
|
||||
const params = {};
|
||||
const where = [];
|
||||
|
||||
if (fromDays > 0) {
|
||||
const fromDate = new Date(now.getTime() - fromDays * 86400000).toISOString();
|
||||
params['@fromDate'] = fromDate;
|
||||
where.push('created_at >= @fromDate');
|
||||
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
data.push({ created_at: dbRow[0], time: dbRow[1] || 0 });
|
||||
},
|
||||
'SELECT created_at, time FROM gamelog_location WHERE created_at < @fromDate ORDER BY created_at DESC LIMIT 1',
|
||||
{ '@fromDate': fromDate }
|
||||
);
|
||||
}
|
||||
if (toDays > 0) {
|
||||
const toDate = new Date(now.getTime() - toDays * 86400000).toISOString();
|
||||
params['@toDate'] = toDate;
|
||||
where.push('created_at < @toDate');
|
||||
}
|
||||
|
||||
const dateClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
data.push({ created_at: dbRow[0], time: dbRow[1] || 0 });
|
||||
},
|
||||
`SELECT created_at, time FROM gamelog_location ${dateClause} ORDER BY created_at`,
|
||||
params
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current user's online sessions after a given timestamp (incremental).
|
||||
* @param {string} afterCreatedAt - Only return rows created after this timestamp
|
||||
* @param {boolean} [inclusive=false] - If true, use >= instead of > to re-read the last record
|
||||
* @returns {Promise<Array<{created_at: string, time: number}>>}
|
||||
*/
|
||||
async getCurrentUserOnlineSessionsAfter(afterCreatedAt) {
|
||||
async getCurrentUserOnlineSessionsAfter(afterCreatedAt, inclusive = false) {
|
||||
const data = [];
|
||||
const op = inclusive ? '>=' : '>';
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
data.push({ created_at: dbRow[0], time: dbRow[1] || 0 });
|
||||
},
|
||||
`SELECT created_at, time FROM gamelog_location WHERE created_at > @after ORDER BY created_at`,
|
||||
`SELECT created_at, time FROM gamelog_location WHERE created_at ${op} @after ORDER BY created_at`,
|
||||
{ '@after': afterCreatedAt }
|
||||
);
|
||||
return data;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { activityCache } from './activityCache.js';
|
||||
import { activityV2 } from './activityV2.js';
|
||||
import { avatarFavorites } from './avatarFavorites.js';
|
||||
import { avatarTags } from './avatarTags.js';
|
||||
import { feed } from './feed.js';
|
||||
@@ -26,7 +26,7 @@ const dbVars = {
|
||||
|
||||
const database = {
|
||||
...feed,
|
||||
...activityCache,
|
||||
...activityV2,
|
||||
...gameLog,
|
||||
...notifications,
|
||||
...moderation,
|
||||
@@ -76,13 +76,71 @@ const database = {
|
||||
`CREATE INDEX IF NOT EXISTS ${dbVars.userPrefix}_feed_online_offline_user_created_idx ON ${dbVars.userPrefix}_feed_online_offline (user_id, created_at)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_activity_cache_meta (user_id TEXT PRIMARY KEY, updated_at TEXT, is_self INTEGER DEFAULT 0, source_last_created_at TEXT, pending_session_start_at INTEGER)`
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_activity_sync_state_v2 (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
updated_at TEXT NOT NULL DEFAULT '',
|
||||
is_self INTEGER NOT NULL DEFAULT 0,
|
||||
source_last_created_at TEXT NOT NULL DEFAULT '',
|
||||
pending_session_start_at INTEGER,
|
||||
cached_range_days INTEGER NOT NULL DEFAULT 0
|
||||
)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_activity_cache_sessions (user_id TEXT NOT NULL, start_at INTEGER NOT NULL, end_at INTEGER NOT NULL, PRIMARY KEY (user_id, start_at, end_at))`
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_activity_sessions_v2 (
|
||||
session_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
start_at INTEGER NOT NULL,
|
||||
end_at INTEGER NOT NULL,
|
||||
is_open_tail INTEGER NOT NULL DEFAULT 0,
|
||||
source_revision TEXT NOT NULL DEFAULT ''
|
||||
)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE INDEX IF NOT EXISTS ${dbVars.userPrefix}_activity_cache_sessions_user_start_idx ON ${dbVars.userPrefix}_activity_cache_sessions (user_id, start_at)`
|
||||
`CREATE INDEX IF NOT EXISTS ${dbVars.userPrefix}_activity_sessions_v2_user_start_idx ON ${dbVars.userPrefix}_activity_sessions_v2 (user_id, start_at)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE INDEX IF NOT EXISTS ${dbVars.userPrefix}_activity_sessions_v2_user_end_idx ON ${dbVars.userPrefix}_activity_sessions_v2 (user_id, end_at)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_activity_range_cache_v2 (
|
||||
user_id TEXT NOT NULL,
|
||||
range_days INTEGER NOT NULL,
|
||||
cache_kind INTEGER NOT NULL,
|
||||
is_complete INTEGER NOT NULL DEFAULT 0,
|
||||
built_from_cursor TEXT NOT NULL DEFAULT '',
|
||||
built_at TEXT NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (user_id, range_days, cache_kind)
|
||||
)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_activity_bucket_cache_v2 (
|
||||
user_id TEXT NOT NULL,
|
||||
target_user_id TEXT NOT NULL DEFAULT '',
|
||||
range_days INTEGER NOT NULL,
|
||||
view_kind TEXT NOT NULL,
|
||||
exclude_key TEXT NOT NULL DEFAULT '',
|
||||
bucket_version INTEGER NOT NULL DEFAULT 1,
|
||||
raw_buckets_json TEXT NOT NULL DEFAULT '[]',
|
||||
normalized_buckets_json TEXT NOT NULL DEFAULT '[]',
|
||||
built_from_cursor TEXT NOT NULL DEFAULT '',
|
||||
summary_json TEXT NOT NULL DEFAULT '{}',
|
||||
built_at TEXT NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (user_id, target_user_id, range_days, view_kind, exclude_key)
|
||||
)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_activity_top_worlds_cache_v2 (
|
||||
user_id TEXT NOT NULL,
|
||||
range_days INTEGER NOT NULL,
|
||||
rank_index INTEGER NOT NULL,
|
||||
world_id TEXT NOT NULL,
|
||||
world_name TEXT NOT NULL,
|
||||
visit_count INTEGER NOT NULL DEFAULT 0,
|
||||
total_time INTEGER NOT NULL DEFAULT 0,
|
||||
built_from_cursor TEXT NOT NULL DEFAULT '',
|
||||
built_at TEXT NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (user_id, range_days, rank_index)
|
||||
)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_friend_log_current (user_id TEXT PRIMARY KEY, display_name TEXT, trust_level TEXT, friend_number INTEGER)`
|
||||
@@ -117,6 +175,9 @@ const database = {
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS gamelog_location (id INTEGER PRIMARY KEY, created_at TEXT, location TEXT, world_id TEXT, world_name TEXT, time INTEGER, group_name TEXT, UNIQUE(created_at, location))`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE INDEX IF NOT EXISTS gamelog_location_created_at_idx ON gamelog_location (created_at)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS gamelog_join_leave (id INTEGER PRIMARY KEY, created_at TEXT, type TEXT, display_name TEXT, location TEXT, user_id TEXT, time INTEGER, UNIQUE(created_at, type, display_name))`
|
||||
);
|
||||
|
||||
@@ -10,16 +10,10 @@ const tableAlter = {
|
||||
await this.updateTableForGroupNames();
|
||||
await this.addFriendLogFriendNumber();
|
||||
await this.updateTableForAvatarHistory();
|
||||
await this.ensureActivityCacheTables();
|
||||
// }
|
||||
// await sqliteService.executeNonQuery('PRAGMA user_version = 1');
|
||||
},
|
||||
|
||||
async updateActivityTabDatabaseVersion() {
|
||||
await this.ensureActivityCacheTables();
|
||||
await this.ensureFeedOnlineOfflineIndexes();
|
||||
},
|
||||
|
||||
async updateTableForGroupNames() {
|
||||
var tables = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
@@ -88,36 +82,6 @@ const tableAlter = {
|
||||
}
|
||||
},
|
||||
|
||||
async ensureActivityCacheTables() {
|
||||
const tables = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
tables.push(dbRow[0]);
|
||||
}, `SELECT name FROM sqlite_schema WHERE type='table' AND name LIKE '%_feed_online_offline'`);
|
||||
for (const tableName of tables) {
|
||||
const userPrefix = tableName.replace(/_feed_online_offline$/, '');
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${userPrefix}_activity_cache_meta (user_id TEXT PRIMARY KEY, updated_at TEXT, is_self INTEGER DEFAULT 0, source_last_created_at TEXT, pending_session_start_at INTEGER)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${userPrefix}_activity_cache_sessions (user_id TEXT NOT NULL, start_at INTEGER NOT NULL, end_at INTEGER NOT NULL, PRIMARY KEY (user_id, start_at, end_at))`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE INDEX IF NOT EXISTS ${userPrefix}_activity_cache_sessions_user_start_idx ON ${userPrefix}_activity_cache_sessions (user_id, start_at)`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async ensureFeedOnlineOfflineIndexes() {
|
||||
const tables = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
tables.push(dbRow[0]);
|
||||
}, `SELECT name FROM sqlite_schema WHERE type='table' AND name LIKE '%_feed_online_offline'`);
|
||||
for (const tableName of tables) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE INDEX IF NOT EXISTS ${tableName}_user_created_idx ON ${tableName} (user_id, created_at)`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { tableAlter };
|
||||
|
||||
Reference in New Issue
Block a user