All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 40s
commit 579f0878e5240dc17db69be1e0b0c0f5af7ef9fe
Author: Arno Kaimbacher <arno.kaimbacher@geosphere.at>
Date: Tue Jun 9 09:25:44 2026 +0200
feat: Refactor error handling in Dataset Edit form and improve validation messages
- Updated error handling in the Dataset Edit form to use a centralized formatError function for displaying validation messages.
- Enhanced user feedback by ensuring that error messages are displayed consistently across various fields.
- Modified the validation rule for arrayContainsTypes to provide clearer error messages for missing main and translated titles/abstracts.
- Introduced a new ValidationService to manage manual construction of validation errors.
- Updated Vite configuration to streamline asset loading and improve performance.
- Adjusted Inertia setup to utilize dynamic imports for page-specific assets.
- Cleaned up unnecessary comments and code in various files for better readability.
commit 5efddc2a58c0e164fef585cc7344c06155dbc2c1
Author: Arno Kaimbacher <arno.kaimbacher@geosphere.at>
Date: Mon Jan 12 17:02:47 2026 +0100
feat: add dataset change detection and form submission composables
- Implemented `useDatasetChangeDetection` for tracking unsaved changes in dataset forms, including comparisons for licenses, basic properties, files, coverage, and more.
- Added `useDatasetFormSubmission` for handling dataset form submissions with validation, success/error handling, and auto-save functionality.
145 lines
5.4 KiB
TypeScript
145 lines
5.4 KiB
TypeScript
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<boolean> {
|
|
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<string> {
|
|
// 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<string> {
|
|
// const uniqueName = await this.generateUniqueName();
|
|
|
|
// const serialToken = JSON.stringify(token);
|
|
// await this.cache.setEx(uniqueName, this.ttl, serialToken);
|
|
// return uniqueName;
|
|
// }
|
|
|
|
// private async generateUniqueName(): Promise<string> {
|
|
// 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<ResumptionToken | null> {
|
|
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;
|
|
}
|
|
}
|