mirror of
https://git.joinfirefish.org/firefish/firefish.git
synced 2024-05-19 06:41:10 +02:00
refactor (backend-rs): never throw an error on ID generation
This commit is contained in:
parent
9db729d734
commit
879d499486
2
packages/backend-rs/index.d.ts
vendored
2
packages/backend-rs/index.d.ts
vendored
|
@ -1129,8 +1129,6 @@ export enum ChatEvent {
|
||||||
Typing = 'typing'
|
Typing = 'typing'
|
||||||
}
|
}
|
||||||
export function publishToChatStream(senderUserId: string, receiverUserId: string, kind: ChatEvent, object: any): void
|
export function publishToChatStream(senderUserId: string, receiverUserId: string, kind: ChatEvent, object: any): void
|
||||||
/** Initializes Cuid2 generator. Must be called before any [create_id]. */
|
|
||||||
export function initIdGenerator(length: number, fingerprint: string): void
|
|
||||||
export function getTimestamp(id: string): number
|
export function getTimestamp(id: string): number
|
||||||
/**
|
/**
|
||||||
* The generated ID results in the form of `[8 chars timestamp] + [cuid2]`.
|
* The generated ID results in the form of `[8 chars timestamp] + [cuid2]`.
|
||||||
|
|
|
@ -310,7 +310,7 @@ if (!nativeBinding) {
|
||||||
throw new Error(`Failed to load native binding`)
|
throw new Error(`Failed to load native binding`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, ChatEvent, publishToChatStream, initIdGenerator, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
|
const { loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, ChatEvent, publishToChatStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
|
||||||
|
|
||||||
module.exports.loadEnv = loadEnv
|
module.exports.loadEnv = loadEnv
|
||||||
module.exports.loadConfig = loadConfig
|
module.exports.loadConfig = loadConfig
|
||||||
|
@ -356,7 +356,6 @@ module.exports.UserProfileFfvisibilityEnum = UserProfileFfvisibilityEnum
|
||||||
module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum
|
module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum
|
||||||
module.exports.ChatEvent = ChatEvent
|
module.exports.ChatEvent = ChatEvent
|
||||||
module.exports.publishToChatStream = publishToChatStream
|
module.exports.publishToChatStream = publishToChatStream
|
||||||
module.exports.initIdGenerator = initIdGenerator
|
|
||||||
module.exports.getTimestamp = getTimestamp
|
module.exports.getTimestamp = getTimestamp
|
||||||
module.exports.genId = genId
|
module.exports.genId = genId
|
||||||
module.exports.genIdAt = genIdAt
|
module.exports.genIdAt = genIdAt
|
||||||
|
|
|
@ -1,21 +1,31 @@
|
||||||
use crate::database::{redis_conn, redis_key};
|
use crate::database::{redis_conn, redis_key};
|
||||||
use crate::model::entity::note;
|
use crate::model::entity::note;
|
||||||
use crate::service::stream;
|
use crate::service::stream;
|
||||||
use crate::util::id::get_timestamp;
|
use crate::util::id::{get_timestamp, InvalidIdErr};
|
||||||
use redis::{streams::StreamMaxlen, Commands};
|
use redis::{streams::StreamMaxlen, Commands, RedisError};
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Redis error: {0}")]
|
||||||
|
RedisErr(#[from] RedisError),
|
||||||
|
#[error("Invalid ID: {0}")]
|
||||||
|
InvalidIdErr(#[from] InvalidIdErr),
|
||||||
|
#[error("Stream error: {0}")]
|
||||||
|
StreamErr(#[from] stream::Error),
|
||||||
|
}
|
||||||
|
|
||||||
type Note = note::Model;
|
type Note = note::Model;
|
||||||
|
|
||||||
#[crate::export]
|
#[crate::export]
|
||||||
pub fn add_note_to_antenna(antenna_id: String, note: &Note) -> Result<(), stream::Error> {
|
pub fn add_note_to_antenna(antenna_id: String, note: &Note) -> Result<(), Error> {
|
||||||
// for timeline API
|
// for timeline API
|
||||||
redis_conn()?.xadd_maxlen(
|
redis_conn()?.xadd_maxlen(
|
||||||
redis_key(format!("antennaTimeline:{}", antenna_id)),
|
redis_key(format!("antennaTimeline:{}", antenna_id)),
|
||||||
StreamMaxlen::Approx(200),
|
StreamMaxlen::Approx(200),
|
||||||
format!("{}-*", get_timestamp(¬e.id)),
|
format!("{}-*", get_timestamp(¬e.id)?),
|
||||||
&[("note", ¬e.id)],
|
&[("note", ¬e.id)],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// for streaming API
|
// for streaming API
|
||||||
stream::antenna::publish(antenna_id, note)
|
Ok(stream::antenna::publish(antenna_id, note)?)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,54 +1,63 @@
|
||||||
//! ID generation utility based on [cuid2]
|
//! ID generation utility based on [cuid2]
|
||||||
|
|
||||||
|
use crate::config::CONFIG;
|
||||||
use basen::BASE36;
|
use basen::BASE36;
|
||||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
|
|
||||||
#[error("ID generator has not been initialized yet")]
|
|
||||||
pub struct ErrorUninitialized;
|
|
||||||
|
|
||||||
static FINGERPRINT: OnceCell<String> = OnceCell::new();
|
static FINGERPRINT: OnceCell<String> = OnceCell::new();
|
||||||
static GENERATOR: OnceCell<cuid2::CuidConstructor> = OnceCell::new();
|
static GENERATOR: OnceCell<cuid2::CuidConstructor> = OnceCell::new();
|
||||||
|
|
||||||
const TIME_2000: i64 = 946_684_800_000;
|
const TIME_2000: i64 = 946_684_800_000;
|
||||||
const TIMESTAMP_LENGTH: u16 = 8;
|
const TIMESTAMP_LENGTH: u8 = 8;
|
||||||
|
|
||||||
/// Initializes Cuid2 generator. Must be called before any [create_id].
|
/// Initializes Cuid2 generator.
|
||||||
#[crate::export]
|
fn init_id_generator(length: u8, fingerprint: &str) {
|
||||||
pub fn init_id_generator(length: u16, fingerprint: &str) {
|
|
||||||
FINGERPRINT.get_or_init(move || format!("{}{}", fingerprint, cuid2::create_id()));
|
FINGERPRINT.get_or_init(move || format!("{}{}", fingerprint, cuid2::create_id()));
|
||||||
GENERATOR.get_or_init(move || {
|
GENERATOR.get_or_init(move || {
|
||||||
cuid2::CuidConstructor::new()
|
cuid2::CuidConstructor::new()
|
||||||
// length to pass shoule be greater than or equal to 8.
|
// length to pass shoule be greater than or equal to 8.
|
||||||
.with_length(cmp::max(length - TIMESTAMP_LENGTH, 8))
|
.with_length(cmp::max(length - TIMESTAMP_LENGTH, 8).into())
|
||||||
.with_fingerprinter(|| FINGERPRINT.get().unwrap().clone())
|
.with_fingerprinter(|| FINGERPRINT.get().unwrap().clone())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns Cuid2 with the length specified by [init_id]. Must be called after
|
/// Returns Cuid2 with the length specified by [init_id_generator].
|
||||||
/// [init_id], otherwise returns [ErrorUninitialized].
|
/// It automatically calls [init_id_generator], if the generator has not been initialized.
|
||||||
pub fn create_id(datetime: &NaiveDateTime) -> Result<String, ErrorUninitialized> {
|
fn create_id(datetime: &NaiveDateTime) -> String {
|
||||||
match GENERATOR.get() {
|
if GENERATOR.get().is_none() {
|
||||||
None => Err(ErrorUninitialized),
|
let length = match &CONFIG.cuid {
|
||||||
Some(gen) => {
|
Some(cuid) => cmp::min(cmp::max(cuid.length.unwrap_or(16), 16), 24),
|
||||||
let date_num = cmp::max(0, datetime.and_utc().timestamp_millis() - TIME_2000) as u64;
|
None => 16,
|
||||||
Ok(format!(
|
};
|
||||||
"{:0>8}{}",
|
let fingerprint = match &CONFIG.cuid {
|
||||||
BASE36.encode_var_len(&date_num),
|
Some(cuid) => cuid.fingerprint.as_deref().unwrap_or_default(),
|
||||||
gen.create_id()
|
None => "",
|
||||||
))
|
};
|
||||||
}
|
init_id_generator(length, fingerprint);
|
||||||
}
|
}
|
||||||
|
let date_num = cmp::max(0, datetime.and_utc().timestamp_millis() - TIME_2000) as u64;
|
||||||
|
format!(
|
||||||
|
"{:0>8}{}",
|
||||||
|
BASE36.encode_var_len(&date_num),
|
||||||
|
GENERATOR.get().unwrap().create_id()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
#[error("Invalid ID: {id}")]
|
||||||
|
pub struct InvalidIdErr {
|
||||||
|
id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[crate::export]
|
#[crate::export]
|
||||||
pub fn get_timestamp(id: &str) -> i64 {
|
pub fn get_timestamp(id: &str) -> Result<i64, InvalidIdErr> {
|
||||||
let n: Option<u64> = BASE36.decode_var_len(&id[0..8]);
|
let n: Option<u64> = BASE36.decode_var_len(&id[0..8]);
|
||||||
match n {
|
if let Some(n) = n {
|
||||||
None => -1,
|
Ok(n as i64 + TIME_2000)
|
||||||
Some(n) => n as i64 + TIME_2000,
|
} else {
|
||||||
|
Err(InvalidIdErr { id: id.to_string() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,35 +69,41 @@ pub fn get_timestamp(id: &str) -> i64 {
|
||||||
/// Ref: https://github.com/paralleldrive/cuid2#parameterized-length
|
/// Ref: https://github.com/paralleldrive/cuid2#parameterized-length
|
||||||
#[crate::export]
|
#[crate::export]
|
||||||
pub fn gen_id() -> String {
|
pub fn gen_id() -> String {
|
||||||
create_id(&Utc::now().naive_utc()).unwrap()
|
create_id(&Utc::now().naive_utc())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate an ID using a specific datetime
|
/// Generate an ID using a specific datetime
|
||||||
#[crate::export]
|
#[crate::export]
|
||||||
pub fn gen_id_at(date: DateTime<Utc>) -> String {
|
pub fn gen_id_at(date: DateTime<Utc>) -> String {
|
||||||
create_id(&date.naive_utc()).unwrap()
|
create_id(&date.naive_utc())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod unit_test {
|
mod unit_test {
|
||||||
use crate::util::id;
|
use super::{gen_id, gen_id_at, get_timestamp};
|
||||||
use chrono::Utc;
|
use chrono::{Duration, Utc};
|
||||||
use pretty_assertions::{assert_eq, assert_ne};
|
use pretty_assertions::{assert_eq, assert_ne};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_create_and_decode_id() {
|
fn can_create_and_decode_id() {
|
||||||
let now = Utc::now().naive_utc();
|
let now = Utc::now();
|
||||||
assert_eq!(id::create_id(&now), Err(id::ErrorUninitialized));
|
assert_eq!(gen_id().len(), 16);
|
||||||
id::init_id_generator(16, "");
|
assert_ne!(gen_id_at(now), gen_id_at(now));
|
||||||
assert_eq!(id::create_id(&now).unwrap().len(), 16);
|
assert_ne!(gen_id(), gen_id());
|
||||||
assert_ne!(id::create_id(&now).unwrap(), id::create_id(&now).unwrap());
|
|
||||||
let id1 = thread::spawn(move || id::create_id(&now).unwrap());
|
let id1 = thread::spawn(move || gen_id_at(now));
|
||||||
let id2 = thread::spawn(move || id::create_id(&now).unwrap());
|
let id2 = thread::spawn(move || gen_id_at(now));
|
||||||
assert_ne!(id1.join().unwrap(), id2.join().unwrap());
|
assert_ne!(id1.join().unwrap(), id2.join().unwrap());
|
||||||
|
|
||||||
let test_id = id::create_id(&now).unwrap();
|
let test_id = gen_id_at(now);
|
||||||
let timestamp = id::get_timestamp(&test_id);
|
let timestamp = get_timestamp(&test_id).unwrap();
|
||||||
assert_eq!(now.and_utc().timestamp_millis(), timestamp);
|
assert_eq!(now.timestamp_millis(), timestamp);
|
||||||
|
|
||||||
|
let now_id = gen_id_at(now);
|
||||||
|
let old_id = gen_id_at(now - Duration::milliseconds(1));
|
||||||
|
let future_id = gen_id_at(now + Duration::milliseconds(1));
|
||||||
|
assert!(old_id < now_id);
|
||||||
|
assert!(now_id < future_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,11 @@
|
||||||
import cluster from "node:cluster";
|
import cluster from "node:cluster";
|
||||||
import { config } from "@/config.js";
|
|
||||||
import { initDb } from "@/db/postgre.js";
|
import { initDb } from "@/db/postgre.js";
|
||||||
import { initIdGenerator } from "backend-rs";
|
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init worker process
|
* Init worker process
|
||||||
*/
|
*/
|
||||||
export async function workerMain() {
|
export async function workerMain() {
|
||||||
const length = Math.min(Math.max(config.cuid?.length ?? 16, 16), 24);
|
|
||||||
const fingerprint = config.cuid?.fingerprint ?? "";
|
|
||||||
initIdGenerator(length, fingerprint);
|
|
||||||
|
|
||||||
await initDb();
|
await initDb();
|
||||||
|
|
||||||
if (!process.env.mode || process.env.mode === "web") {
|
if (!process.env.mode || process.env.mode === "web") {
|
||||||
|
|
Loading…
Reference in a new issue