fix: defer plugin runtime globals until use
This commit is contained in:
parent
43513cd1df
commit
8a05c05596
@ -51,16 +51,18 @@ type FeishuThreadBindingsState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FEISHU_THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.feishuThreadBindingsState");
|
const FEISHU_THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.feishuThreadBindingsState");
|
||||||
const state = resolveGlobalSingleton<FeishuThreadBindingsState>(
|
let state: FeishuThreadBindingsState | undefined;
|
||||||
FEISHU_THREAD_BINDINGS_STATE_KEY,
|
|
||||||
() => ({
|
|
||||||
managersByAccountId: new Map(),
|
|
||||||
bindingsByAccountConversation: new Map(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const MANAGERS_BY_ACCOUNT_ID = state.managersByAccountId;
|
function getState(): FeishuThreadBindingsState {
|
||||||
const BINDINGS_BY_ACCOUNT_CONVERSATION = state.bindingsByAccountConversation;
|
state ??= resolveGlobalSingleton<FeishuThreadBindingsState>(
|
||||||
|
FEISHU_THREAD_BINDINGS_STATE_KEY,
|
||||||
|
() => ({
|
||||||
|
managersByAccountId: new Map(),
|
||||||
|
bindingsByAccountConversation: new Map(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveBindingKey(params: { accountId: string; conversationId: string }): string {
|
function resolveBindingKey(params: { accountId: string; conversationId: string }): string {
|
||||||
return `${params.accountId}:${params.conversationId}`;
|
return `${params.accountId}:${params.conversationId}`;
|
||||||
@ -119,7 +121,7 @@ export function createFeishuThreadBindingManager(params: {
|
|||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
}): FeishuThreadBindingManager {
|
}): FeishuThreadBindingManager {
|
||||||
const accountId = normalizeAccountId(params.accountId);
|
const accountId = normalizeAccountId(params.accountId);
|
||||||
const existing = MANAGERS_BY_ACCOUNT_ID.get(accountId);
|
const existing = getState().managersByAccountId.get(accountId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
@ -138,9 +140,11 @@ export function createFeishuThreadBindingManager(params: {
|
|||||||
const manager: FeishuThreadBindingManager = {
|
const manager: FeishuThreadBindingManager = {
|
||||||
accountId,
|
accountId,
|
||||||
getByConversationId: (conversationId) =>
|
getByConversationId: (conversationId) =>
|
||||||
BINDINGS_BY_ACCOUNT_CONVERSATION.get(resolveBindingKey({ accountId, conversationId })),
|
getState().bindingsByAccountConversation.get(
|
||||||
|
resolveBindingKey({ accountId, conversationId }),
|
||||||
|
),
|
||||||
listBySessionKey: (targetSessionKey) =>
|
listBySessionKey: (targetSessionKey) =>
|
||||||
[...BINDINGS_BY_ACCOUNT_CONVERSATION.values()].filter(
|
[...getState().bindingsByAccountConversation.values()].filter(
|
||||||
(record) => record.accountId === accountId && record.targetSessionKey === targetSessionKey,
|
(record) => record.accountId === accountId && record.targetSessionKey === targetSessionKey,
|
||||||
),
|
),
|
||||||
bindConversation: ({
|
bindConversation: ({
|
||||||
@ -184,7 +188,7 @@ export function createFeishuThreadBindingManager(params: {
|
|||||||
boundAt: now,
|
boundAt: now,
|
||||||
lastActivityAt: now,
|
lastActivityAt: now,
|
||||||
};
|
};
|
||||||
BINDINGS_BY_ACCOUNT_CONVERSATION.set(
|
getState().bindingsByAccountConversation.set(
|
||||||
resolveBindingKey({ accountId, conversationId: normalizedConversationId }),
|
resolveBindingKey({ accountId, conversationId: normalizedConversationId }),
|
||||||
record,
|
record,
|
||||||
);
|
);
|
||||||
@ -192,30 +196,30 @@ export function createFeishuThreadBindingManager(params: {
|
|||||||
},
|
},
|
||||||
touchConversation: (conversationId, at = Date.now()) => {
|
touchConversation: (conversationId, at = Date.now()) => {
|
||||||
const key = resolveBindingKey({ accountId, conversationId });
|
const key = resolveBindingKey({ accountId, conversationId });
|
||||||
const existingRecord = BINDINGS_BY_ACCOUNT_CONVERSATION.get(key);
|
const existingRecord = getState().bindingsByAccountConversation.get(key);
|
||||||
if (!existingRecord) {
|
if (!existingRecord) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const updated = { ...existingRecord, lastActivityAt: at };
|
const updated = { ...existingRecord, lastActivityAt: at };
|
||||||
BINDINGS_BY_ACCOUNT_CONVERSATION.set(key, updated);
|
getState().bindingsByAccountConversation.set(key, updated);
|
||||||
return updated;
|
return updated;
|
||||||
},
|
},
|
||||||
unbindConversation: (conversationId) => {
|
unbindConversation: (conversationId) => {
|
||||||
const key = resolveBindingKey({ accountId, conversationId });
|
const key = resolveBindingKey({ accountId, conversationId });
|
||||||
const existingRecord = BINDINGS_BY_ACCOUNT_CONVERSATION.get(key);
|
const existingRecord = getState().bindingsByAccountConversation.get(key);
|
||||||
if (!existingRecord) {
|
if (!existingRecord) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
BINDINGS_BY_ACCOUNT_CONVERSATION.delete(key);
|
getState().bindingsByAccountConversation.delete(key);
|
||||||
return existingRecord;
|
return existingRecord;
|
||||||
},
|
},
|
||||||
unbindBySessionKey: (targetSessionKey) => {
|
unbindBySessionKey: (targetSessionKey) => {
|
||||||
const removed: FeishuThreadBindingRecord[] = [];
|
const removed: FeishuThreadBindingRecord[] = [];
|
||||||
for (const record of [...BINDINGS_BY_ACCOUNT_CONVERSATION.values()]) {
|
for (const record of [...getState().bindingsByAccountConversation.values()]) {
|
||||||
if (record.accountId !== accountId || record.targetSessionKey !== targetSessionKey) {
|
if (record.accountId !== accountId || record.targetSessionKey !== targetSessionKey) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
BINDINGS_BY_ACCOUNT_CONVERSATION.delete(
|
getState().bindingsByAccountConversation.delete(
|
||||||
resolveBindingKey({ accountId, conversationId: record.conversationId }),
|
resolveBindingKey({ accountId, conversationId: record.conversationId }),
|
||||||
);
|
);
|
||||||
removed.push(record);
|
removed.push(record);
|
||||||
@ -223,12 +227,12 @@ export function createFeishuThreadBindingManager(params: {
|
|||||||
return removed;
|
return removed;
|
||||||
},
|
},
|
||||||
stop: () => {
|
stop: () => {
|
||||||
for (const key of [...BINDINGS_BY_ACCOUNT_CONVERSATION.keys()]) {
|
for (const key of [...getState().bindingsByAccountConversation.keys()]) {
|
||||||
if (key.startsWith(`${accountId}:`)) {
|
if (key.startsWith(`${accountId}:`)) {
|
||||||
BINDINGS_BY_ACCOUNT_CONVERSATION.delete(key);
|
getState().bindingsByAccountConversation.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MANAGERS_BY_ACCOUNT_ID.delete(accountId);
|
getState().managersByAccountId.delete(accountId);
|
||||||
unregisterSessionBindingAdapter({ channel: "feishu", accountId });
|
unregisterSessionBindingAdapter({ channel: "feishu", accountId });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -290,22 +294,22 @@ export function createFeishuThreadBindingManager(params: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
MANAGERS_BY_ACCOUNT_ID.set(accountId, manager);
|
getState().managersByAccountId.set(accountId, manager);
|
||||||
return manager;
|
return manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFeishuThreadBindingManager(
|
export function getFeishuThreadBindingManager(
|
||||||
accountId?: string,
|
accountId?: string,
|
||||||
): FeishuThreadBindingManager | null {
|
): FeishuThreadBindingManager | null {
|
||||||
return MANAGERS_BY_ACCOUNT_ID.get(normalizeAccountId(accountId)) ?? null;
|
return getState().managersByAccountId.get(normalizeAccountId(accountId)) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const __testing = {
|
export const __testing = {
|
||||||
resetFeishuThreadBindingsForTests() {
|
resetFeishuThreadBindingsForTests() {
|
||||||
for (const manager of MANAGERS_BY_ACCOUNT_ID.values()) {
|
for (const manager of getState().managersByAccountId.values()) {
|
||||||
manager.stop();
|
manager.stop();
|
||||||
}
|
}
|
||||||
MANAGERS_BY_ACCOUNT_ID.clear();
|
getState().managersByAccountId.clear();
|
||||||
BINDINGS_BY_ACCOUNT_CONVERSATION.clear();
|
getState().bindingsByAccountConversation.clear();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,12 @@ const MAX_ENTRIES = 5000;
|
|||||||
*/
|
*/
|
||||||
const SLACK_THREAD_PARTICIPATION_KEY = Symbol.for("openclaw.slackThreadParticipation");
|
const SLACK_THREAD_PARTICIPATION_KEY = Symbol.for("openclaw.slackThreadParticipation");
|
||||||
|
|
||||||
const threadParticipation = resolveGlobalMap<string, number>(SLACK_THREAD_PARTICIPATION_KEY);
|
let threadParticipation: Map<string, number> | undefined;
|
||||||
|
|
||||||
|
function getThreadParticipation(): Map<string, number> {
|
||||||
|
threadParticipation ??= resolveGlobalMap<string, number>(SLACK_THREAD_PARTICIPATION_KEY);
|
||||||
|
return threadParticipation;
|
||||||
|
}
|
||||||
|
|
||||||
function makeKey(accountId: string, channelId: string, threadTs: string): string {
|
function makeKey(accountId: string, channelId: string, threadTs: string): string {
|
||||||
return `${accountId}:${channelId}:${threadTs}`;
|
return `${accountId}:${channelId}:${threadTs}`;
|
||||||
@ -23,17 +28,17 @@ function makeKey(accountId: string, channelId: string, threadTs: string): string
|
|||||||
|
|
||||||
function evictExpired(): void {
|
function evictExpired(): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const [key, timestamp] of threadParticipation) {
|
for (const [key, timestamp] of getThreadParticipation()) {
|
||||||
if (now - timestamp > TTL_MS) {
|
if (now - timestamp > TTL_MS) {
|
||||||
threadParticipation.delete(key);
|
getThreadParticipation().delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function evictOldest(): void {
|
function evictOldest(): void {
|
||||||
const oldest = threadParticipation.keys().next().value;
|
const oldest = getThreadParticipation().keys().next().value;
|
||||||
if (oldest) {
|
if (oldest) {
|
||||||
threadParticipation.delete(oldest);
|
getThreadParticipation().delete(oldest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +50,7 @@ export function recordSlackThreadParticipation(
|
|||||||
if (!accountId || !channelId || !threadTs) {
|
if (!accountId || !channelId || !threadTs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const threadParticipation = getThreadParticipation();
|
||||||
if (threadParticipation.size >= MAX_ENTRIES) {
|
if (threadParticipation.size >= MAX_ENTRIES) {
|
||||||
evictExpired();
|
evictExpired();
|
||||||
}
|
}
|
||||||
@ -63,6 +69,7 @@ export function hasSlackThreadParticipation(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const key = makeKey(accountId, channelId, threadTs);
|
const key = makeKey(accountId, channelId, threadTs);
|
||||||
|
const threadParticipation = getThreadParticipation();
|
||||||
const timestamp = threadParticipation.get(key);
|
const timestamp = threadParticipation.get(key);
|
||||||
if (timestamp == null) {
|
if (timestamp == null) {
|
||||||
return false;
|
return false;
|
||||||
@ -75,5 +82,5 @@ export function hasSlackThreadParticipation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearSlackThreadParticipationCache(): void {
|
export function clearSlackThreadParticipationCache(): void {
|
||||||
threadParticipation.clear();
|
getThreadParticipation().clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,11 +28,17 @@ type TelegramSendMessageDraft = (
|
|||||||
*/
|
*/
|
||||||
const TELEGRAM_DRAFT_STREAM_STATE_KEY = Symbol.for("openclaw.telegramDraftStreamState");
|
const TELEGRAM_DRAFT_STREAM_STATE_KEY = Symbol.for("openclaw.telegramDraftStreamState");
|
||||||
|
|
||||||
const draftStreamState = resolveGlobalSingleton(TELEGRAM_DRAFT_STREAM_STATE_KEY, () => ({
|
let draftStreamState: { nextDraftId: number } | undefined;
|
||||||
nextDraftId: 0,
|
|
||||||
}));
|
function getDraftStreamState(): { nextDraftId: number } {
|
||||||
|
draftStreamState ??= resolveGlobalSingleton(TELEGRAM_DRAFT_STREAM_STATE_KEY, () => ({
|
||||||
|
nextDraftId: 0,
|
||||||
|
}));
|
||||||
|
return draftStreamState;
|
||||||
|
}
|
||||||
|
|
||||||
function allocateTelegramDraftId(): number {
|
function allocateTelegramDraftId(): number {
|
||||||
|
const draftStreamState = getDraftStreamState();
|
||||||
draftStreamState.nextDraftId =
|
draftStreamState.nextDraftId =
|
||||||
draftStreamState.nextDraftId >= TELEGRAM_DRAFT_ID_MAX ? 1 : draftStreamState.nextDraftId + 1;
|
draftStreamState.nextDraftId >= TELEGRAM_DRAFT_ID_MAX ? 1 : draftStreamState.nextDraftId + 1;
|
||||||
return draftStreamState.nextDraftId;
|
return draftStreamState.nextDraftId;
|
||||||
@ -454,6 +460,6 @@ export function createTelegramDraftStream(params: {
|
|||||||
|
|
||||||
export const __testing = {
|
export const __testing = {
|
||||||
resetTelegramDraftStreamForTests() {
|
resetTelegramDraftStreamForTests() {
|
||||||
draftStreamState.nextDraftId = 0;
|
getDraftStreamState().nextDraftId = 0;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -103,17 +103,34 @@ function escapeRegex(str: string): string {
|
|||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
|
||||||
const FILE_EXTENSIONS_PATTERN = Array.from(FILE_REF_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
|
|
||||||
const AUTO_LINKED_ANCHOR_PATTERN = /<a\s+href="https?:\/\/([^"]+)"[^>]*>\1<\/a>/gi;
|
const AUTO_LINKED_ANCHOR_PATTERN = /<a\s+href="https?:\/\/([^"]+)"[^>]*>\1<\/a>/gi;
|
||||||
const FILE_REFERENCE_PATTERN = new RegExp(
|
|
||||||
`(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=$|[^a-zA-Z0-9_\\-/])`,
|
|
||||||
"gi",
|
|
||||||
);
|
|
||||||
const ORPHANED_TLD_PATTERN = new RegExp(
|
|
||||||
`([^a-zA-Z0-9]|^)([A-Za-z]\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=[^a-zA-Z0-9/]|$)`,
|
|
||||||
"g",
|
|
||||||
);
|
|
||||||
const HTML_TAG_PATTERN = /(<\/?)([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*?>/gi;
|
const HTML_TAG_PATTERN = /(<\/?)([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*?>/gi;
|
||||||
|
let fileReferencePattern: RegExp | undefined;
|
||||||
|
let orphanedTldPattern: RegExp | undefined;
|
||||||
|
|
||||||
|
function getFileReferencePattern(): RegExp {
|
||||||
|
if (fileReferencePattern) {
|
||||||
|
return fileReferencePattern;
|
||||||
|
}
|
||||||
|
const fileExtensionsPattern = Array.from(FILE_REF_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
|
||||||
|
fileReferencePattern = new RegExp(
|
||||||
|
`(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${fileExtensionsPattern}))(?=$|[^a-zA-Z0-9_\\-/])`,
|
||||||
|
"gi",
|
||||||
|
);
|
||||||
|
return fileReferencePattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrphanedTldPattern(): RegExp {
|
||||||
|
if (orphanedTldPattern) {
|
||||||
|
return orphanedTldPattern;
|
||||||
|
}
|
||||||
|
const fileExtensionsPattern = Array.from(FILE_REF_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
|
||||||
|
orphanedTldPattern = new RegExp(
|
||||||
|
`([^a-zA-Z0-9]|^)([A-Za-z]\\.(?:${fileExtensionsPattern}))(?=[^a-zA-Z0-9/]|$)`,
|
||||||
|
"g",
|
||||||
|
);
|
||||||
|
return orphanedTldPattern;
|
||||||
|
}
|
||||||
|
|
||||||
function wrapStandaloneFileRef(match: string, prefix: string, filename: string): string {
|
function wrapStandaloneFileRef(match: string, prefix: string, filename: string): string {
|
||||||
if (filename.startsWith("//")) {
|
if (filename.startsWith("//")) {
|
||||||
@ -134,8 +151,8 @@ function wrapSegmentFileRefs(
|
|||||||
if (!text || codeDepth > 0 || preDepth > 0 || anchorDepth > 0) {
|
if (!text || codeDepth > 0 || preDepth > 0 || anchorDepth > 0) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
const wrappedStandalone = text.replace(FILE_REFERENCE_PATTERN, wrapStandaloneFileRef);
|
const wrappedStandalone = text.replace(getFileReferencePattern(), wrapStandaloneFileRef);
|
||||||
return wrappedStandalone.replace(ORPHANED_TLD_PATTERN, (match, prefix: string, tld: string) =>
|
return wrappedStandalone.replace(getOrphanedTldPattern(), (match, prefix: string, tld: string) =>
|
||||||
prefix === ">" ? match : `${prefix}<code>${escapeHtml(tld)}</code>`,
|
prefix === ">" ? match : `${prefix}<code>${escapeHtml(tld)}</code>`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,12 @@ type CacheEntry = {
|
|||||||
*/
|
*/
|
||||||
const TELEGRAM_SENT_MESSAGES_KEY = Symbol.for("openclaw.telegramSentMessages");
|
const TELEGRAM_SENT_MESSAGES_KEY = Symbol.for("openclaw.telegramSentMessages");
|
||||||
|
|
||||||
const sentMessages = resolveGlobalMap<string, CacheEntry>(TELEGRAM_SENT_MESSAGES_KEY);
|
let sentMessages: Map<string, CacheEntry> | undefined;
|
||||||
|
|
||||||
|
function getSentMessages(): Map<string, CacheEntry> {
|
||||||
|
sentMessages ??= resolveGlobalMap<string, CacheEntry>(TELEGRAM_SENT_MESSAGES_KEY);
|
||||||
|
return sentMessages;
|
||||||
|
}
|
||||||
|
|
||||||
function getChatKey(chatId: number | string): string {
|
function getChatKey(chatId: number | string): string {
|
||||||
return String(chatId);
|
return String(chatId);
|
||||||
@ -37,6 +42,7 @@ function cleanupExpired(entry: CacheEntry): void {
|
|||||||
*/
|
*/
|
||||||
export function recordSentMessage(chatId: number | string, messageId: number): void {
|
export function recordSentMessage(chatId: number | string, messageId: number): void {
|
||||||
const key = getChatKey(chatId);
|
const key = getChatKey(chatId);
|
||||||
|
const sentMessages = getSentMessages();
|
||||||
let entry = sentMessages.get(key);
|
let entry = sentMessages.get(key);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
entry = { timestamps: new Map() };
|
entry = { timestamps: new Map() };
|
||||||
@ -54,7 +60,7 @@ export function recordSentMessage(chatId: number | string, messageId: number): v
|
|||||||
*/
|
*/
|
||||||
export function wasSentByBot(chatId: number | string, messageId: number): boolean {
|
export function wasSentByBot(chatId: number | string, messageId: number): boolean {
|
||||||
const key = getChatKey(chatId);
|
const key = getChatKey(chatId);
|
||||||
const entry = sentMessages.get(key);
|
const entry = getSentMessages().get(key);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -67,5 +73,5 @@ export function wasSentByBot(chatId: number | string, messageId: number): boolea
|
|||||||
* Clear all cached entries (for testing).
|
* Clear all cached entries (for testing).
|
||||||
*/
|
*/
|
||||||
export function clearSentMessageCache(): void {
|
export function clearSentMessageCache(): void {
|
||||||
sentMessages.clear();
|
getSentMessages().clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,17 +77,19 @@ type TelegramThreadBindingsState = {
|
|||||||
*/
|
*/
|
||||||
const TELEGRAM_THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.telegramThreadBindingsState");
|
const TELEGRAM_THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.telegramThreadBindingsState");
|
||||||
|
|
||||||
const threadBindingsState = resolveGlobalSingleton<TelegramThreadBindingsState>(
|
let threadBindingsState: TelegramThreadBindingsState | undefined;
|
||||||
TELEGRAM_THREAD_BINDINGS_STATE_KEY,
|
|
||||||
() => ({
|
function getThreadBindingsState(): TelegramThreadBindingsState {
|
||||||
managersByAccountId: new Map<string, TelegramThreadBindingManager>(),
|
threadBindingsState ??= resolveGlobalSingleton<TelegramThreadBindingsState>(
|
||||||
bindingsByAccountConversation: new Map<string, TelegramThreadBindingRecord>(),
|
TELEGRAM_THREAD_BINDINGS_STATE_KEY,
|
||||||
persistQueueByAccountId: new Map<string, Promise<void>>(),
|
() => ({
|
||||||
}),
|
managersByAccountId: new Map<string, TelegramThreadBindingManager>(),
|
||||||
);
|
bindingsByAccountConversation: new Map<string, TelegramThreadBindingRecord>(),
|
||||||
const MANAGERS_BY_ACCOUNT_ID = threadBindingsState.managersByAccountId;
|
persistQueueByAccountId: new Map<string, Promise<void>>(),
|
||||||
const BINDINGS_BY_ACCOUNT_CONVERSATION = threadBindingsState.bindingsByAccountConversation;
|
}),
|
||||||
const PERSIST_QUEUE_BY_ACCOUNT_ID = threadBindingsState.persistQueueByAccountId;
|
);
|
||||||
|
return threadBindingsState;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeDurationMs(raw: unknown, fallback: number): number {
|
function normalizeDurationMs(raw: unknown, fallback: number): number {
|
||||||
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
||||||
@ -168,7 +170,7 @@ function fromSessionBindingInput(params: {
|
|||||||
}): TelegramThreadBindingRecord {
|
}): TelegramThreadBindingRecord {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const metadata = params.input.metadata ?? {};
|
const metadata = params.input.metadata ?? {};
|
||||||
const existing = BINDINGS_BY_ACCOUNT_CONVERSATION.get(
|
const existing = getThreadBindingsState().bindingsByAccountConversation.get(
|
||||||
resolveBindingKey({
|
resolveBindingKey({
|
||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
conversationId: params.input.conversationId,
|
conversationId: params.input.conversationId,
|
||||||
@ -310,7 +312,7 @@ async function persistBindingsToDisk(params: {
|
|||||||
version: STORE_VERSION,
|
version: STORE_VERSION,
|
||||||
bindings:
|
bindings:
|
||||||
params.bindings ??
|
params.bindings ??
|
||||||
[...BINDINGS_BY_ACCOUNT_CONVERSATION.values()].filter(
|
[...getThreadBindingsState().bindingsByAccountConversation.values()].filter(
|
||||||
(entry) => entry.accountId === params.accountId,
|
(entry) => entry.accountId === params.accountId,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@ -322,7 +324,7 @@ async function persistBindingsToDisk(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function listBindingsForAccount(accountId: string): TelegramThreadBindingRecord[] {
|
function listBindingsForAccount(accountId: string): TelegramThreadBindingRecord[] {
|
||||||
return [...BINDINGS_BY_ACCOUNT_CONVERSATION.values()].filter(
|
return [...getThreadBindingsState().bindingsByAccountConversation.values()].filter(
|
||||||
(entry) => entry.accountId === accountId,
|
(entry) => entry.accountId === accountId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -335,16 +337,17 @@ function enqueuePersistBindings(params: {
|
|||||||
if (!params.persist) {
|
if (!params.persist) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
const previous = PERSIST_QUEUE_BY_ACCOUNT_ID.get(params.accountId) ?? Promise.resolve();
|
const previous =
|
||||||
|
getThreadBindingsState().persistQueueByAccountId.get(params.accountId) ?? Promise.resolve();
|
||||||
const next = previous
|
const next = previous
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await persistBindingsToDisk(params);
|
await persistBindingsToDisk(params);
|
||||||
});
|
});
|
||||||
PERSIST_QUEUE_BY_ACCOUNT_ID.set(params.accountId, next);
|
getThreadBindingsState().persistQueueByAccountId.set(params.accountId, next);
|
||||||
void next.finally(() => {
|
void next.finally(() => {
|
||||||
if (PERSIST_QUEUE_BY_ACCOUNT_ID.get(params.accountId) === next) {
|
if (getThreadBindingsState().persistQueueByAccountId.get(params.accountId) === next) {
|
||||||
PERSIST_QUEUE_BY_ACCOUNT_ID.delete(params.accountId);
|
getThreadBindingsState().persistQueueByAccountId.delete(params.accountId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return next;
|
return next;
|
||||||
@ -412,7 +415,7 @@ export function createTelegramThreadBindingManager(
|
|||||||
} = {},
|
} = {},
|
||||||
): TelegramThreadBindingManager {
|
): TelegramThreadBindingManager {
|
||||||
const accountId = normalizeAccountId(params.accountId);
|
const accountId = normalizeAccountId(params.accountId);
|
||||||
const existing = MANAGERS_BY_ACCOUNT_ID.get(accountId);
|
const existing = getThreadBindingsState().managersByAccountId.get(accountId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
@ -430,7 +433,7 @@ export function createTelegramThreadBindingManager(
|
|||||||
accountId,
|
accountId,
|
||||||
conversationId: entry.conversationId,
|
conversationId: entry.conversationId,
|
||||||
});
|
});
|
||||||
BINDINGS_BY_ACCOUNT_CONVERSATION.set(key, {
|
getThreadBindingsState().bindingsByAccountConversation.set(key, {
|
||||||
...entry,
|
...entry,
|
||||||
accountId,
|
accountId,
|
||||||
});
|
});
|
||||||
@ -448,7 +451,7 @@ export function createTelegramThreadBindingManager(
|
|||||||
if (!conversationId) {
|
if (!conversationId) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return BINDINGS_BY_ACCOUNT_CONVERSATION.get(
|
return getThreadBindingsState().bindingsByAccountConversation.get(
|
||||||
resolveBindingKey({
|
resolveBindingKey({
|
||||||
accountId,
|
accountId,
|
||||||
conversationId,
|
conversationId,
|
||||||
@ -471,7 +474,7 @@ export function createTelegramThreadBindingManager(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const key = resolveBindingKey({ accountId, conversationId });
|
const key = resolveBindingKey({ accountId, conversationId });
|
||||||
const existing = BINDINGS_BY_ACCOUNT_CONVERSATION.get(key);
|
const existing = getThreadBindingsState().bindingsByAccountConversation.get(key);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -479,7 +482,7 @@ export function createTelegramThreadBindingManager(
|
|||||||
...existing,
|
...existing,
|
||||||
lastActivityAt: normalizeTimestampMs(at ?? Date.now()),
|
lastActivityAt: normalizeTimestampMs(at ?? Date.now()),
|
||||||
};
|
};
|
||||||
BINDINGS_BY_ACCOUNT_CONVERSATION.set(key, nextRecord);
|
getThreadBindingsState().bindingsByAccountConversation.set(key, nextRecord);
|
||||||
persistBindingsSafely({
|
persistBindingsSafely({
|
||||||
accountId,
|
accountId,
|
||||||
persist: manager.shouldPersistMutations(),
|
persist: manager.shouldPersistMutations(),
|
||||||
@ -494,11 +497,11 @@ export function createTelegramThreadBindingManager(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const key = resolveBindingKey({ accountId, conversationId });
|
const key = resolveBindingKey({ accountId, conversationId });
|
||||||
const removed = BINDINGS_BY_ACCOUNT_CONVERSATION.get(key) ?? null;
|
const removed = getThreadBindingsState().bindingsByAccountConversation.get(key) ?? null;
|
||||||
if (!removed) {
|
if (!removed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
BINDINGS_BY_ACCOUNT_CONVERSATION.delete(key);
|
getThreadBindingsState().bindingsByAccountConversation.delete(key);
|
||||||
persistBindingsSafely({
|
persistBindingsSafely({
|
||||||
accountId,
|
accountId,
|
||||||
persist: manager.shouldPersistMutations(),
|
persist: manager.shouldPersistMutations(),
|
||||||
@ -521,7 +524,7 @@ export function createTelegramThreadBindingManager(
|
|||||||
accountId,
|
accountId,
|
||||||
conversationId: entry.conversationId,
|
conversationId: entry.conversationId,
|
||||||
});
|
});
|
||||||
BINDINGS_BY_ACCOUNT_CONVERSATION.delete(key);
|
getThreadBindingsState().bindingsByAccountConversation.delete(key);
|
||||||
removed.push(entry);
|
removed.push(entry);
|
||||||
}
|
}
|
||||||
if (removed.length > 0) {
|
if (removed.length > 0) {
|
||||||
@ -540,9 +543,9 @@ export function createTelegramThreadBindingManager(
|
|||||||
sweepTimer = null;
|
sweepTimer = null;
|
||||||
}
|
}
|
||||||
unregisterSessionBindingAdapter({ channel: "telegram", accountId });
|
unregisterSessionBindingAdapter({ channel: "telegram", accountId });
|
||||||
const existingManager = MANAGERS_BY_ACCOUNT_ID.get(accountId);
|
const existingManager = getThreadBindingsState().managersByAccountId.get(accountId);
|
||||||
if (existingManager === manager) {
|
if (existingManager === manager) {
|
||||||
MANAGERS_BY_ACCOUNT_ID.delete(accountId);
|
getThreadBindingsState().managersByAccountId.delete(accountId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -574,7 +577,7 @@ export function createTelegramThreadBindingManager(
|
|||||||
metadata: input.metadata,
|
metadata: input.metadata,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
BINDINGS_BY_ACCOUNT_CONVERSATION.set(
|
getThreadBindingsState().bindingsByAccountConversation.set(
|
||||||
resolveBindingKey({ accountId, conversationId }),
|
resolveBindingKey({ accountId, conversationId }),
|
||||||
record,
|
record,
|
||||||
);
|
);
|
||||||
@ -714,14 +717,14 @@ export function createTelegramThreadBindingManager(
|
|||||||
sweepTimer.unref?.();
|
sweepTimer.unref?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
MANAGERS_BY_ACCOUNT_ID.set(accountId, manager);
|
getThreadBindingsState().managersByAccountId.set(accountId, manager);
|
||||||
return manager;
|
return manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTelegramThreadBindingManager(
|
export function getTelegramThreadBindingManager(
|
||||||
accountId?: string,
|
accountId?: string,
|
||||||
): TelegramThreadBindingManager | null {
|
): TelegramThreadBindingManager | null {
|
||||||
return MANAGERS_BY_ACCOUNT_ID.get(normalizeAccountId(accountId)) ?? null;
|
return getThreadBindingsState().managersByAccountId.get(normalizeAccountId(accountId)) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTelegramBindingsBySessionKey(params: {
|
function updateTelegramBindingsBySessionKey(params: {
|
||||||
@ -741,7 +744,7 @@ function updateTelegramBindingsBySessionKey(params: {
|
|||||||
conversationId: entry.conversationId,
|
conversationId: entry.conversationId,
|
||||||
});
|
});
|
||||||
const next = params.update(entry, now);
|
const next = params.update(entry, now);
|
||||||
BINDINGS_BY_ACCOUNT_CONVERSATION.set(key, next);
|
getThreadBindingsState().bindingsByAccountConversation.set(key, next);
|
||||||
updated.push(next);
|
updated.push(next);
|
||||||
}
|
}
|
||||||
if (updated.length > 0) {
|
if (updated.length > 0) {
|
||||||
@ -799,12 +802,12 @@ export function setTelegramThreadBindingMaxAgeBySessionKey(params: {
|
|||||||
|
|
||||||
export const __testing = {
|
export const __testing = {
|
||||||
async resetTelegramThreadBindingsForTests() {
|
async resetTelegramThreadBindingsForTests() {
|
||||||
for (const manager of MANAGERS_BY_ACCOUNT_ID.values()) {
|
for (const manager of getThreadBindingsState().managersByAccountId.values()) {
|
||||||
manager.stop();
|
manager.stop();
|
||||||
}
|
}
|
||||||
await Promise.allSettled(PERSIST_QUEUE_BY_ACCOUNT_ID.values());
|
await Promise.allSettled(getThreadBindingsState().persistQueueByAccountId.values());
|
||||||
PERSIST_QUEUE_BY_ACCOUNT_ID.clear();
|
getThreadBindingsState().persistQueueByAccountId.clear();
|
||||||
MANAGERS_BY_ACCOUNT_ID.clear();
|
getThreadBindingsState().managersByAccountId.clear();
|
||||||
BINDINGS_BY_ACCOUNT_CONVERSATION.clear();
|
getThreadBindingsState().bindingsByAccountConversation.clear();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user