wip
This commit is contained in:
parent
f5563c8304
commit
aa8296ef11
|
@ -2384,6 +2384,14 @@ export interface Locale extends ILocale {
|
||||||
* スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。
|
* スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。
|
||||||
*/
|
*/
|
||||||
"scratchpadDescription": string;
|
"scratchpadDescription": string;
|
||||||
|
/**
|
||||||
|
* UIインスペクター
|
||||||
|
*/
|
||||||
|
"uiInspector": string;
|
||||||
|
/**
|
||||||
|
* メモリ上に存在しているUIコンポーネントのインスタンスの一覧を見ることができます。UIコンポーネントはUi:C:系関数により生成されます。
|
||||||
|
*/
|
||||||
|
"uiInspectorDescription": string;
|
||||||
/**
|
/**
|
||||||
* 出力
|
* 出力
|
||||||
*/
|
*/
|
||||||
|
@ -5575,6 +5583,10 @@ export interface Locale extends ILocale {
|
||||||
* 有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。
|
* 有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。
|
||||||
*/
|
*/
|
||||||
"fanoutTimelineDbFallbackDescription": string;
|
"fanoutTimelineDbFallbackDescription": string;
|
||||||
|
/**
|
||||||
|
* 有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。
|
||||||
|
*/
|
||||||
|
"reactionsBufferingDescription": string;
|
||||||
/**
|
/**
|
||||||
* 問い合わせ先URL
|
* 問い合わせ先URL
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1411,6 +1411,7 @@ _serverSettings:
|
||||||
fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
|
fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
|
||||||
fanoutTimelineDbFallback: "データベースへのフォールバック"
|
fanoutTimelineDbFallback: "データベースへのフォールバック"
|
||||||
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
|
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
|
||||||
|
reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
|
||||||
inquiryUrl: "問い合わせ先URL"
|
inquiryUrl: "問い合わせ先URL"
|
||||||
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
|
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
|
||||||
export class ReactionService {
|
export class ReactionService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redis)
|
||||||
private redisClient: Redis.Redis,
|
private redisClient: Redis.Redis, // TODO: 専用のRedisインスタンスにする
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
@ -174,7 +174,6 @@ export class ReactionService {
|
||||||
reaction,
|
reaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create reaction
|
|
||||||
try {
|
try {
|
||||||
await this.noteReactionsRepository.insert(record);
|
await this.noteReactionsRepository.insert(record);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -197,17 +196,23 @@ export class ReactionService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rbt = true;
|
||||||
|
|
||||||
// Increment reactions count
|
// Increment reactions count
|
||||||
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
|
if (rbt) {
|
||||||
await this.notesRepository.createQueryBuilder().update()
|
this.redisClient.hincrby(`reactionsBuffer:${note.id}`, reaction, 1);
|
||||||
.set({
|
} else {
|
||||||
reactions: () => sql,
|
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
|
||||||
...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
|
await this.notesRepository.createQueryBuilder().update()
|
||||||
reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
|
.set({
|
||||||
} : {}),
|
reactions: () => sql,
|
||||||
})
|
...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
|
||||||
.where('id = :id', { id: note.id })
|
reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
|
||||||
.execute();
|
} : {}),
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: note.id })
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
// 30%の確率、セルフではない、3日以内に投稿されたノートの場合ハイライト用ランキング更新
|
// 30%の確率、セルフではない、3日以内に投稿されたノートの場合ハイライト用ランキング更新
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
|
@ -22,6 +23,18 @@ import type { ReactionService } from '../ReactionService.js';
|
||||||
import type { UserEntityService } from './UserEntityService.js';
|
import type { UserEntityService } from './UserEntityService.js';
|
||||||
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||||
|
|
||||||
|
function mergeReactions(src: Record<string, number>, delta: Record<string, number>) {
|
||||||
|
const reactions = { ...src };
|
||||||
|
for (const [name, count] of Object.entries(delta)) {
|
||||||
|
if (reactions[name] != null) {
|
||||||
|
reactions[name] += count;
|
||||||
|
} else {
|
||||||
|
reactions[name] = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reactions;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteEntityService implements OnModuleInit {
|
export class NoteEntityService implements OnModuleInit {
|
||||||
private userEntityService: UserEntityService;
|
private userEntityService: UserEntityService;
|
||||||
|
@ -34,6 +47,9 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
|
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis, // TODO: 専用のRedisインスタンスにする
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -287,6 +303,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
skipHide?: boolean;
|
skipHide?: boolean;
|
||||||
withReactionAndUserPairCache?: boolean;
|
withReactionAndUserPairCache?: boolean;
|
||||||
_hint_?: {
|
_hint_?: {
|
||||||
|
reactionsDeltas: Map<MiNote['id'], Record<string, number>>;
|
||||||
myReactions: Map<MiNote['id'], string | null>;
|
myReactions: Map<MiNote['id'], string | null>;
|
||||||
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
|
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
|
||||||
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
|
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
|
||||||
|
@ -315,7 +332,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
: await this.channelsRepository.findOneBy({ id: note.channelId })
|
: await this.channelsRepository.findOneBy({ id: note.channelId })
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const reactionEmojiNames = Object.keys(note.reactions)
|
const reactionEmojiNames = Object.keys(mergeReactions(note.reactions, opts._hint_?.reactionsDeltas.get(note.id) ?? {}))
|
||||||
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
|
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
|
||||||
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
|
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
|
||||||
const packedFiles = options?._hint_?.packedFiles;
|
const packedFiles = options?._hint_?.packedFiles;
|
||||||
|
@ -334,8 +351,8 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
|
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
|
||||||
renoteCount: note.renoteCount,
|
renoteCount: note.renoteCount,
|
||||||
repliesCount: note.repliesCount,
|
repliesCount: note.repliesCount,
|
||||||
reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0),
|
reactionCount: Object.values(mergeReactions(note.reactions, opts._hint_?.reactionsDeltas.get(note.id) ?? {})).reduce((a, b) => a + b, 0),
|
||||||
reactions: this.reactionService.convertLegacyReactions(note.reactions),
|
reactions: mergeReactions(this.reactionService.convertLegacyReactions(note.reactions), opts._hint_?.reactionsDeltas.get(note.id) ?? {}),
|
||||||
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
|
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
|
||||||
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
|
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
|
||||||
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
|
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
|
||||||
|
@ -376,7 +393,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
|
|
||||||
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
|
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
|
||||||
|
|
||||||
...(meId && Object.keys(note.reactions).length > 0 ? {
|
...(meId && Object.keys(mergeReactions(note.reactions, opts._hint_?.reactionsDeltas.get(note.id) ?? {})).length > 0 ? {
|
||||||
myReaction: this.populateMyReaction(note, meId, options?._hint_),
|
myReaction: this.populateMyReaction(note, meId, options?._hint_),
|
||||||
} : {}),
|
} : {}),
|
||||||
} : {}),
|
} : {}),
|
||||||
|
@ -400,6 +417,28 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
) {
|
) {
|
||||||
if (notes.length === 0) return [];
|
if (notes.length === 0) return [];
|
||||||
|
|
||||||
|
const reactionsDeltas = new Map<MiNote['id'], Record<string, number>>();
|
||||||
|
|
||||||
|
const rbt = true;
|
||||||
|
|
||||||
|
if (rbt) {
|
||||||
|
const pipeline = this.redisClient.pipeline();
|
||||||
|
for (const note of notes) {
|
||||||
|
pipeline.hgetall(`reactionsBuffer:${note.id}`);
|
||||||
|
}
|
||||||
|
const results = await pipeline.exec();
|
||||||
|
|
||||||
|
for (let i = 0; i < notes.length; i++) {
|
||||||
|
const note = notes[i];
|
||||||
|
const result = results![i][1];
|
||||||
|
const delta = {};
|
||||||
|
for (const [name, count] of Object.entries(result)) {
|
||||||
|
delta[name] = parseInt(count);
|
||||||
|
}
|
||||||
|
reactionsDeltas.set(note.id, delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
const myReactionsMap = new Map<MiNote['id'], string | null>();
|
const myReactionsMap = new Map<MiNote['id'], string | null>();
|
||||||
if (meId) {
|
if (meId) {
|
||||||
|
@ -410,7 +449,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
|
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
|
if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
|
||||||
const reactionsCount = Object.values(note.renote.reactions).reduce((a, b) => a + b, 0);
|
const reactionsCount = Object.values(mergeReactions(note.renote.reactions, reactionsDeltas.get(note.renote.id) ?? {})).reduce((a, b) => a + b, 0);
|
||||||
if (reactionsCount === 0) {
|
if (reactionsCount === 0) {
|
||||||
myReactionsMap.set(note.renote.id, null);
|
myReactionsMap.set(note.renote.id, null);
|
||||||
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) {
|
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) {
|
||||||
|
@ -421,7 +460,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (note.id < oldId) {
|
if (note.id < oldId) {
|
||||||
const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
|
const reactionsCount = Object.values(mergeReactions(note.reactions, reactionsDeltas.get(note.id) ?? {})).reduce((a, b) => a + b, 0);
|
||||||
if (reactionsCount === 0) {
|
if (reactionsCount === 0) {
|
||||||
myReactionsMap.set(note.id, null);
|
myReactionsMap.set(note.id, null);
|
||||||
} else if (reactionsCount <= note.reactionAndUserPairCache.length) {
|
} else if (reactionsCount <= note.reactionAndUserPairCache.length) {
|
||||||
|
@ -461,6 +500,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
return await Promise.all(notes.map(n => this.pack(n, me, {
|
return await Promise.all(notes.map(n => this.pack(n, me, {
|
||||||
...options,
|
...options,
|
||||||
_hint_: {
|
_hint_: {
|
||||||
|
reactionsDeltas,
|
||||||
myReactions: myReactionsMap,
|
myReactions: myReactionsMap,
|
||||||
packedFiles,
|
packedFiles,
|
||||||
packedUsers,
|
packedUsers,
|
||||||
|
|
|
@ -589,6 +589,19 @@ export class MiMeta {
|
||||||
})
|
})
|
||||||
public perUserListTimelineCacheMax: number;
|
public perUserListTimelineCacheMax: number;
|
||||||
|
|
||||||
|
// @Column('boolean', {
|
||||||
|
// default: true,
|
||||||
|
// })
|
||||||
|
// public enableReactionsBuffering: boolean;
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 分
|
||||||
|
// */
|
||||||
|
// @Column('integer', {
|
||||||
|
// default: 180,
|
||||||
|
// })
|
||||||
|
// public reactionsBufferingTtl: number;
|
||||||
|
|
||||||
@Column('integer', {
|
@Column('integer', {
|
||||||
default: 0,
|
default: 0,
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue