import ResumptionToken from './ResumptionToken.js'; import { createClient, RedisClientType } from 'redis'; import InternalServerErrorException from '#app/exceptions/InternalServerException'; import { sprintf } from 'sprintf-js'; import dayjs from 'dayjs'; import TokenWorkerContract from './TokenWorkerContract.js'; export default class TokenWorkerService implements TokenWorkerContract { protected filePrefix = 'rs_'; protected fileExtension = 'txt'; private cache: RedisClientType; public ttl: number; private url: string; private connected = false; constructor(ttl: number) { this.ttl = ttl; // time to live this.url = process.env.REDIS_URL || 'redis://127.0.0.1:6379'; } public async connect() { this.cache = createClient({ url: this.url }); this.cache.on('error', (err) => { this.connected = false; console.log('[Redis] Redis Client Error: ', err); }); this.cache.on('connect', () => { this.connected = true; }); await this.cache.connect(); } public get isConnected(): boolean { return this.connected; } public async has(key: string): Promise { const result = await this.cache.get(key); return result !== undefined && result !== null; } /** * Simplified set method that stores the token using a browser fingerprint key. * If the token for that fingerprint already exists and its documentIds match the new token, * then the fingerprint key is simply returned. */ public async set(token: ResumptionToken, browserFingerprint: string): Promise { // Generate a 15-digit unique number string based on the fingerprint const uniqueNumberKey = this.createUniqueNumberFromFingerprint(browserFingerprint, token.documentIds, token.totalIds); // Optionally, you could prefix it if desired, e.g. 'rs_' + uniqueNumberKey const fingerprintKey = uniqueNumberKey; // const fingerprintKey = `rs_fp_${browserFingerprint}`; const existingTokenString = await this.cache.get(fingerprintKey); if (existingTokenString) { const existingToken = this.parseToken(existingTokenString); if (this.arraysAreEqual(existingToken.documentIds, token.documentIds)) { return fingerprintKey; } } const serialToken = JSON.stringify(token); await this.cache.setEx(fingerprintKey, this.ttl, serialToken); return fingerprintKey; } // Updated helper method to generate a unique key based on fingerprint and documentIds private createUniqueNumberFromFingerprint(browserFingerprint: string, documentIds: number[], totalIds: number): string { // Combine the fingerprint, document IDs and totalIds to produce the input string const combined = browserFingerprint + ':' + documentIds.join('-') + ':' + totalIds; // Simple hash algorithm let hash = 0; for (let i = 0; i < combined.length; i++) { hash = (hash << 5) - hash + combined.charCodeAt(i); hash |= 0; // Convert to 32-bit integer } // Ensure positive number and limit it to at most 15 digits const positiveHash = Math.abs(hash) % 1000000000000000; // Pad with trailing zeros to ensure a 15-digit string return positiveHash.toString().padEnd(15, '0'); } // Add a helper function to compare two arrays of numbers with identical order private arraysAreEqual(arr1: number[], arr2: number[]): boolean { if (arr1.length !== arr2.length) { return false; } return arr1.every((num, index) => num === arr2[index]); } // public async set(token: ResumptionToken): Promise { // const uniqueName = await this.generateUniqueName(); // const serialToken = JSON.stringify(token); // await this.cache.setEx(uniqueName, this.ttl, serialToken); // return uniqueName; // } private async generateUniqueName(): Promise { let fc = 0; const uniqueId = dayjs().unix().toString(); let uniqueName: string; let cacheKeyExists: boolean; do { // format values // %s - String // %d - Signed decimal number (negative, zero or positive) // [0-9] (Specifies the minimum width held of to the variable value) uniqueName = sprintf('%s%05d', uniqueId, fc++); cacheKeyExists = await this.has(uniqueName); } while (cacheKeyExists); return uniqueName; } public async get(key: string): Promise { if (!this.cache) { throw new InternalServerErrorException('Dataset is not available for OAI export!'); } const result = await this.cache.get(key); return result ? this.parseToken(result) : null; } private parseToken(result: string): ResumptionToken { const rToken: ResumptionToken = new ResumptionToken(); const parsed = JSON.parse(result); Object.assign(rToken, parsed); return rToken; } public del(key: string) { this.cache.del(key); } public flush() { this.cache.flushAll(); } public async close() { await this.cache.disconnect(); this.connected = false; } }