import path from "node:path" ;
import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe" ;
const DEFAULT_REPLAY_TTL_MS = 24 * 60 * 60 * 1000 ;
const DEFAULT_MEMORY_MAX_SIZE = 1 _000 ;
const DEFAULT_FILE_MAX_ENTRIES = 10 _000 ;
function sanitizeSegment(value: string): string {
const trimmed = value.trim();
if (!trimmed) {
return "default" ;
}
return trimmed.replace(/[^a-zA-Z0-9 _-]/g, "_" );
}
function buildReplayKey(params: { roomToken: string; messageId: string }): string | null {
const roomToken = params.roomToken.trim();
const messageId = params.messageId.trim();
if (!roomToken || !messageId) {
return null ;
}
return `${roomToken}:${messageId}`;
}
export type NextcloudTalkReplayGuardOptions = {
stateDir?: string;
ttlMs?: number;
memoryMaxSize?: number;
fileMaxEntries?: number;
onDiskError?: (error: unknown) => void ;
};
export type NextcloudTalkReplayGuard = {
claimMessage: (params: {
accountId: string;
roomToken: string;
messageId: string;
}) => Promise<"claimed" | "duplicate" | "inflight" | "invalid" >;
commitMessage: (params: {
accountId: string;
roomToken: string;
messageId: string;
}) => Promise<boolean >;
releaseMessage: (params: {
accountId: string;
roomToken: string;
messageId: string;
error?: unknown;
}) => void ;
shouldProcessMessage: (params: {
accountId: string;
roomToken: string;
messageId: string;
}) => Promise<boolean >;
};
export function createNextcloudTalkReplayGuard(
options: NextcloudTalkReplayGuardOptions,
): NextcloudTalkReplayGuard {
const stateDir = options.stateDir?.trim();
const baseOptions = {
ttlMs: options.ttlMs ?? DEFAULT_REPLAY_TTL_MS,
memoryMaxSize: options.memoryMaxSize ?? DEFAULT_MEMORY_MAX_SIZE,
};
const dedupe = createClaimableDedupe(
stateDir
? {
...baseOptions,
fileMaxEntries: options.fileMaxEntries ?? DEFAULT_FILE_MAX_ENTRIES,
resolveFilePath: (namespace) =>
path.join(
stateDir,
"nextcloud-talk" ,
"replay-dedupe" ,
`${sanitizeSegment(namespace)}.json`,
),
onDiskError: options.onDiskError,
}
: baseOptions,
);
return {
claimMessage: async ({ accountId, roomToken, messageId }) => {
const replayKey = buildReplayKey({ roomToken, messageId });
if (!replayKey) {
return "invalid" ;
}
const result = await dedupe.claim(replayKey, {
namespace: accountId,
});
return result.kind;
},
commitMessage: async ({ accountId, roomToken, messageId }) => {
const replayKey = buildReplayKey({ roomToken, messageId });
if (!replayKey) {
return true ;
}
return await dedupe.commit(replayKey, {
namespace: accountId,
});
},
releaseMessage: ({ accountId, roomToken, messageId, error }) => {
const replayKey = buildReplayKey({ roomToken, messageId });
if (!replayKey) {
return ;
}
dedupe.release(replayKey, {
namespace: accountId,
error,
});
},
shouldProcessMessage: async ({ accountId, roomToken, messageId }) => {
const replayKey = buildReplayKey({ roomToken, messageId });
if (!replayKey) {
return true ;
}
const result = await dedupe.claim(replayKey, {
namespace: accountId,
});
if (result.kind !== "claimed" ) {
return false ;
}
return await dedupe.commit(replayKey, {
namespace: accountId,
});
},
};
}
Messung V0.5 in Prozent C=99 H=98 G=98
¤ Dauer der Verarbeitung: 0.10 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland