master
veypi 1 year ago
parent c187b078b0
commit 85aab14cba

@ -23,9 +23,6 @@ dropTag:
run:
go run *.go -d
.PHONY:build
build:
@GOOS=linux GOARCH=amd64 go build -o ./build/OneAuth
syncDB:
@scp -P 19529 oa.db root@alco.host:/root/

247
oab/Cargo.lock generated

@ -381,6 +381,12 @@ dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "arrayvec"
version = "0.7.4"
@ -393,6 +399,40 @@ version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
[[package]]
name = "async-nats"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e45b67ea596bb94741ef15ba1d90b72c92bdc07553d8033734cb620a2b39f1c"
dependencies = [
"base64 0.21.4",
"bytes",
"futures",
"http",
"memchr",
"nkeys",
"nuid",
"once_cell",
"rand",
"regex",
"ring",
"rustls 0.21.7",
"rustls-native-certs",
"rustls-pemfile",
"rustls-webpki",
"serde",
"serde_json",
"serde_nanos",
"serde_repr",
"thiserror",
"time",
"tokio",
"tokio-retry",
"tokio-rustls 0.24.1",
"tracing",
"url",
]
[[package]]
name = "async-stream"
version = "0.3.5"
@ -655,6 +695,9 @@ name = "bytes"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
dependencies = [
"serde",
]
[[package]]
name = "bytestring"
@ -773,6 +816,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
@ -875,6 +928,33 @@ dependencies = [
"cipher",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"platforms",
"rustc_version",
"subtle",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]]
name = "darling"
version = "0.20.3"
@ -910,6 +990,12 @@ dependencies = [
"syn 2.0.37",
]
[[package]]
name = "data-encoding"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "dav-server"
version = "0.5.7"
@ -1041,6 +1127,27 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
dependencies = [
"curve25519-dalek",
"ed25519",
"sha2",
"signature",
]
[[package]]
name = "either"
version = "1.9.0"
@ -1109,6 +1216,12 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "fiat-crypto"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d"
[[package]]
name = "finl_unicode"
version = "1.2.0"
@ -1813,6 +1926,22 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "nkeys"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aad178aad32087b19042ee36dfd450b73f5f934fbfb058b59b198684dfec4c47"
dependencies = [
"byteorder",
"data-encoding",
"ed25519",
"ed25519-dalek",
"getrandom",
"log",
"rand",
"signatory",
]
[[package]]
name = "nom"
version = "7.1.3"
@ -1833,6 +1962,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "nuid"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83"
dependencies = [
"rand",
]
[[package]]
name = "num-bigint"
version = "0.3.3"
@ -1922,8 +2060,11 @@ dependencies = [
"actix-multipart",
"actix-web",
"aes-gcm",
"anyhow",
"async-nats",
"base64 0.13.1",
"block-padding",
"bytes",
"chrono",
"clap",
"dav-server",
@ -1935,6 +2076,7 @@ dependencies = [
"jsonwebtoken",
"lazy_static",
"mime_guess",
"nkeys",
"proc",
"rand",
"rust-embed",
@ -1974,6 +2116,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "ordered-float"
version = "3.9.1"
@ -2193,6 +2341,12 @@ version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "platforms"
version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8"
[[package]]
name = "polyval"
version = "0.5.3"
@ -2576,11 +2730,24 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8"
dependencies = [
"log",
"ring",
"rustls-webpki",
"sct 0.7.0",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.3"
@ -2615,6 +2782,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
dependencies = [
"windows-sys",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -2735,6 +2911,29 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "security-framework"
version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "1.0.19"
@ -2781,6 +2980,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_nanos"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae801b7733ca8d6a2b580debe99f67f36826a0f5b8a36055dc6bc40f8d6bc71"
dependencies = [
"serde",
]
[[package]]
name = "serde_plain"
version = "1.0.2"
@ -2888,11 +3096,23 @@ dependencies = [
"libc",
]
[[package]]
name = "signatory"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31"
dependencies = [
"pkcs8 0.10.2",
"rand_core",
"signature",
"zeroize",
]
[[package]]
name = "signature"
version = "2.1.0"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d"
dependencies = [
"digest",
"rand_core",
@ -3276,7 +3496,7 @@ checksum = "4db708cd3e459078f85f39f96a00960bd841f66ee2a669e90bf36907f5a79aae"
dependencies = [
"once_cell",
"tokio",
"tokio-rustls",
"tokio-rustls 0.22.0",
]
[[package]]
@ -3498,6 +3718,17 @@ dependencies = [
"syn 2.0.37",
]
[[package]]
name = "tokio-retry"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f"
dependencies = [
"pin-project",
"rand",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.22.0"
@ -3509,6 +3740,16 @@ dependencies = [
"webpki",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls 0.21.7",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.14"

@ -56,4 +56,8 @@ actix-cors = "0.6.4"
rust-embed = "8.0.0"
mime_guess = "2.0.4"
service-manager = "0.3.0"
async-nats = "0.32.1"
anyhow = "1.0.75"
bytes = "1.5.0"
nkeys = "0.3.2"

@ -15,3 +15,9 @@ sqlx:
entity:
@sea-orm-cli generate entity --database-url mysql://root:123456@localhost:3306/oneauth -o ./src/models/entity/ --with-serde both --model-extra-derives Default,sqlx::FromRow --serde-skip-hidden-column --date-time-crate chrono
# install_target:
# @rustup target add x86_64-unknown-linux-gnu
build_linux:
@cargo build --release --target-dir ./target/linux/

@ -5,64 +5,107 @@
// Distributed under terms of the MIT license.
//
use actix_web::{get, web, Responder};
use proc::access_read;
use actix_web::{post, web, Responder};
use nkeys;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use serde::{Deserialize, Serialize};
use tracing::info;
use crate::{
models::{self, AUStatus, Token, UserPlugin},
models::{self, AUStatus, AccessCore, UserPlugin},
AppState, Error, Result,
};
#[get("/app/{aid}/token/")]
#[access_read("app")]
#[derive(Debug, Deserialize, Serialize)]
pub struct GetOptions {
app_id: Option<String>,
token: String,
nonce: Option<String>,
}
// 转换token
#[post("/app/{aid}/token/")]
pub async fn get(
aid: web::Path<String>,
stat: web::Data<AppState>,
t: web::ReqData<Token>,
query: web::Json<GetOptions>,
) -> Result<impl Responder> {
let n = aid.into_inner();
if !n.is_empty() {
let s = models::app_user::Entity::find()
.filter(models::app_user::Column::AppId.eq(&n))
.filter(models::app_user::Column::UserId.eq(&t.id))
.one(stat.db())
.await?;
if s.is_none() {
return Err(Error::NotAuthed);
};
let s = s.unwrap();
if s.status == AUStatus::OK as i32 {
let result = sqlx::query_as::<_, models::AccessCore>(
let aid = aid.into_inner();
let mut key = stat.key.clone();
let sid = match &query.app_id {
Some(i) => {
if !i.is_empty() {
match models::app::Entity::find_by_id(i).one(stat.db()).await? {
Some(sapp) => key = sapp.key,
None => {}
}
info!("{}", key);
};
i
}
_ => "",
};
info!("{}", key);
let token = models::Token::from(&query.token, &key)?;
if aid.starts_with("nats") {
let nonce = &query.nonce.clone().unwrap();
let u = nkeys::KeyPair::from_seed(&stat.nats_secret).unwrap();
let res = base64::encode(u.sign(nonce.as_bytes()).unwrap());
return Ok(res);
};
if !aid.is_empty() {
// 从OA token 转向其他app token
if sid.is_empty() {
let s = models::app_user::Entity::find()
.filter(models::app_user::Column::AppId.eq(&aid))
.filter(models::app_user::Column::UserId.eq(&token.id))
.one(stat.db())
.await?;
if s.is_none() {
return Err(Error::NotAuthed);
};
let s = s.unwrap();
if s.status == AUStatus::OK as i32 {
let result = sqlx::query_as::<_, models::AccessCore>(
"select access.name, access.rid, access.level from access, user_role, role WHERE user_role.user_id = ? && access.role_id=user_role.role_id && role.id=user_role.role_id && role.app_id = ?",
)
.bind(&t.id)
.bind(&n)
.bind(&token.id)
.bind(&aid)
.fetch_all(stat.sqlx())
.await?;
let appobj = models::app::Entity::find_by_id(&n)
.one(stat.db())
.await?
.unwrap();
let u = models::user::Entity::find_by_id(&t.id)
let appobj = models::app::Entity::find_by_id(&aid)
.one(stat.db())
.await?
.unwrap();
let u = models::user::Entity::find_by_id(&token.id)
.one(stat.db())
.await?
.unwrap();
let str = u.token(result).to_string(&appobj.key)?;
Ok(str)
} else {
Err(Error::NotAuthed)
}
} else {
let u = models::user::Entity::find_by_id(&token.id)
.one(stat.db())
.await?
.unwrap();
let str = u.token(result).to_string(&appobj.key)?;
// tokio::spawn(async move {
// let mut interval = tokio::time::interval(Duration::from_secs(5));
// interval.tick().await;
// let start = Instant::now();
// println!("time:{:?}", start);
// loop {
// interval.tick().await;
// println!("time:{:?}", start.elapsed());
// }
// });
let str = u
.token(vec![
AccessCore {
name: "app".to_string(),
rid: None,
level: models::AccessLevel::Read,
},
AccessCore {
name: "user".to_string(),
rid: None,
level: models::AccessLevel::Read,
},
])
.to_string(&stat.key)?;
Ok(str)
} else {
Err(Error::NotAuthed)
}
} else {
Err(Error::Missing("id".to_string()))

@ -26,7 +26,6 @@ lazy_static! {
pub static ref CLI: AppCli = AppCli::new();
}
pub static mut KEY: String = String::new();
// lazy_static! {
// pub static ref CONFIG: ApplicationConfig = ApplicationConfig::new();
// }
@ -116,11 +115,13 @@ pub struct AppState {
pub db_name: String,
pub log_dir: Option<String>,
pub fs_root: String,
pub nats_key: String,
pub nats_secret: String,
/// "100MB" 日志分割尺寸-单位KB,MB,GB
pub log_temp_size: Option<String>,
pub log_pack_compress: Option<String>,
pub log_level: Option<String>,
pub jwt_secret: Option<String>,
pub user_init_space: i64,
#[serde(skip)]
@ -136,9 +137,6 @@ impl AppState {
Ok(f) => f,
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
// res.connect_sqlx().unwrap();
unsafe {
KEY = res.key.clone();
}
return res;
}
Err(e) => panic!("{}", e),
@ -154,9 +152,6 @@ impl AppState {
} else {
println!("release_mode is enable!")
}
unsafe {
KEY = res.key.clone();
}
res
}
pub fn defaut() -> Self {
@ -175,9 +170,10 @@ impl AppState {
media_path: "/Users/veypi/test/media".to_string(),
fs_root: "/Users/veypi/test/media".to_string(),
log_level: None,
jwt_secret: None,
_sqlx: None,
_db: None,
nats_key: "UCXFAAVMCPTATZUZX6H24YF6FI3NKPQBPLM6BNN2EDFPNSUUEZPNFKEL".to_string(),
nats_secret: "SUACQNAAFKDKRBXS62J4JYZ7DWZS7UNUQI52BOFGGBUACHTDHRQP7I66GI".to_string(),
user_init_space: 300,
}
}

@ -5,6 +5,8 @@
// Distributed under terms of the Apache license.
//
use bytes::Bytes;
use actix_files as fs;
use actix_web::{
dev::{self, Service},
@ -25,13 +27,13 @@ use tracing::{error, info, warn};
async fn main() -> Result<()> {
std::env::set_var("RUST_LOG", "debug");
std::env::set_var("RUST_BACKTRACE", "1");
std::env::set_var("asd", "asd");
init_log();
let mut data = AppState::new();
data.connect().await?;
data.connect_sqlx()?;
if let Some(c) = &CLI.command {
match c {
Clis::Init => {
data.connect_sqlx()?;
models::init(data).await;
return Ok(());
}
@ -41,10 +43,24 @@ async fn main() -> Result<()> {
_ => {}
};
};
data.connect().await?;
data.connect_sqlx()?;
web(data).await?;
Ok(())
}
async fn web(data: AppState) -> Result<()> {
let client = match async_nats::ConnectOptions::new()
.nkey(data.nats_secret.clone())
.connect("127.0.0.1:4222")
.await
{
Ok(r) => r,
Err(e) => return Err(oab::Error::Unknown),
};
client
.publish("msg".to_string(), Bytes::from("asd"))
.await
.unwrap();
let url = data.server_url.clone();
let dav = libs::fs::core();
let serv = HttpServer::new(move || {
@ -61,7 +77,13 @@ async fn web(data: AppState) -> Result<()> {
)
.into()
});
let cors = actix_cors::Cors::permissive();
let cors = actix_cors::Cors::default()
.allow_any_method()
.allow_any_header()
.supports_credentials()
.allowed_origin_fn(|_, _| {
return true;
});
let app = App::new();
app.wrap(logger)
.wrap(middleware::Compress::default())
@ -85,7 +107,7 @@ async fn web(data: AppState) -> Result<()> {
.wrap_fn(|req, srv| {
let headers = &req.headers().clone();
let origin = match headers.get("Origin") {
Some(o) => o.to_str().unwrap().clone().to_string(),
Some(o) => o.to_str().unwrap().to_string(),
None => "".to_string(),
};
srv.call(req).map(move |res| {
@ -129,6 +151,7 @@ struct Asset;
#[actix_web::get("/{_:.*}")]
async fn index(p: web::Path<String>) -> impl Responder {
info!("{}", p);
let p = &p.into_inner();
match Asset::get(p) {
Some(content) => HttpResponse::Ok()

@ -66,7 +66,7 @@ impl UserPlugin for super::entity::user::Model {
exp: (Utc::now() + Duration::days(4)).timestamp(),
iat: Utc::now().timestamp(),
id: self.id.clone(),
ico: self.icon.as_ref().unwrap_or(&default_ico).to_string(),
icon: self.icon.as_ref().unwrap_or(&default_ico).to_string(),
access: Some(ac),
nickname: self
.nickname
@ -173,7 +173,7 @@ pub struct Token {
pub iat: i64, // Optional. 发布时间
pub id: String, // 用户id
pub nickname: String,
pub ico: String,
pub icon: String,
pub access: Option<Vec<AccessCore>>,
}

@ -41,7 +41,7 @@ where
// pub type AsyncResult<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
#[derive(Clone, ThisError, Debug, Deserialize, Serialize)]
#[derive(ThisError, Debug)]
pub enum Error {
// system
// EnvVarError,
@ -128,6 +128,9 @@ pub enum Error {
#[error("unknown error")]
Unknown,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl From<std::io::Error> for Error {

@ -17,7 +17,7 @@
"@toast-ui/editor": "^3.2.2",
"@types/validator": "^13.11.2",
"@veypi/msg": "^0.1.1",
"@veypi/oaer": "^0.0.1",
"@veypi/oaer": "^0.0.3",
"@veypi/one-icon": "2",
"animate.css": "^4.1.1",
"axios": "^1.2.1",
@ -25,8 +25,10 @@
"js-base64": "^3.7.5",
"mathjax": "3",
"mitt": "^3.0.1",
"nats.ws": "^1.18.0",
"pinia": "^2.0.11",
"quasar": "^2.6.0",
"ts-nkeys": "^1.0.16",
"validator": "^13.11.0",
"vite-plugin-rewrite-all": "^1.0.1",
"vue": "^3.0.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

@ -40,6 +40,8 @@ module.exports = configure(function(/* ctx */) {
'i18n',
'api',
'pack',
'nats',
'oaer',
],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css

@ -0,0 +1,40 @@
/*
* nats.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-16 16:18
* Distributed under terms of the MIT license.
*/
import axios from 'axios'
import util from '../libs/util'
import { connect, StringCodec } from '../libs/nats.ws'
const sc = StringCodec();
const nc = await connect({
servers: 'ws://127.0.0.1:4221',
authenticator: function(nonce?: string) {
let nkey = 'UCXFAAVMCPTATZUZX6H24YF6FI3NKPQBPLM6BNN2EDFPNSUUEZPNFKEL'
// let nre = nkeyAuthenticator(nkey_seed)
let res = {
nkey: nkey,
sig: async function() {
let response = await axios.post('/api/app/nats/token/', { token: util.getToken(), nonce: nonce });
console.log(response)
return response.data
}
};
return res
} as any
})
nc.publish('msg', '123')
const sub = nc.subscribe("msg");
(async () => {
for await (const m of sub) {
console.log(`[${sub.getProcessed()}]: ${sc.decode(m.data)}`);
}
console.log("subscription closed");
})();

@ -0,0 +1,22 @@
/*
* oaer.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-16 21:20
* Distributed under terms of the MIT license.
*/
// import '@veypi/oaer'
import oaer from '@veypi/oaer'
import bus from 'src/libs/bus'
import util from 'src/libs/util'
oaer.set({
token: util.getToken(),
host: 'http://' + window.location.host,
uuid: 'FR9P5t8debxc11aFF',
})
bus.on('token', (t: any) => {
oaer.set({ token: t })
})

@ -7,33 +7,11 @@
import { boot } from 'quasar/wrappers'
import '@veypi/msg/index.css'
import { conf } from '@veypi/msg'
import '../assets/icon.js'
import '@veypi/oaer/dist/index.css'
import 'cherry-markdown/dist/cherry-markdown.css';
import oafs from 'src/libs/oafs'
import { Cfg } from '@veypi/oaer'
import util from 'src/libs/util.js'
import evt from 'src/libs/evt.js'
oafs.setCfg({ token: util.getToken(), app_id: 'FR9P5t8debxc11aFF' })
Cfg.token.value = util.getToken()
conf.timeout = 5000
Cfg.host.value = 'http://' + window.location.host
Cfg.uuid.value = 'FR9P5t8debxc11aFF'
evt.on('token', (t: any) => {
oafs.setCfg({ token: t })
Cfg.token.value = t
})
// "async" is optional;
// more info on params: https://v2.quasar.dev/quasar-cli/boot-files
export default boot(async (/* { app, router, ... } */) => {
// something to do
})

@ -28,7 +28,7 @@
<script lang="ts" setup>
import FsTree from './FsTree.vue'
import { ref } from 'vue';
import oafs, { fileProps } from 'src/libs/oafs';
import { oafs, fileProps } from '@veypi/oaer';
import { util } from 'src/libs';

@ -15,7 +15,7 @@
import Cherry from 'cherry-markdown';
import options from './options'
import { computed, onMounted, ref, watch } from 'vue';
import oafs from 'src/libs/oafs';
import { oafs } from '@veypi/oaer'
let editor = {} as Cherry;
let emits = defineEmits<{

@ -7,7 +7,7 @@
<script lang="ts" setup>
import { ref } from 'vue';
import oafs from 'src/libs/oafs';
import {oafs} from '@veypi/oaer'
let file = ref<HTMLInputElement>()
let emits = defineEmits<{

@ -8,5 +8,5 @@
import mitt from "mitt";
const evt = mitt()
export default evt
const bus = mitt()
export default bus

File diff suppressed because it is too large Load Diff

@ -0,0 +1,153 @@
/*
* Copyright 2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
checkJsError,
isFlowControlMsg,
isHeartbeatMsg,
millis,
nanos,
} from "./jsutil.ts";
export {
AdvisoryKind,
consumerOpts,
DirectMsgHeaders,
isConsumerOptsBuilder,
JsHeaders,
RepublishHeaders,
} from "./types.ts";
export type {
Advisory,
Closed,
ConsumerInfoable,
ConsumerOpts,
ConsumerOptsBuilder,
Consumers,
Destroyable,
JetStreamClient,
JetStreamManager,
JetStreamManagerOptions,
JetStreamOptions,
JetStreamPublishOptions,
JetStreamPullSubscription,
JetStreamSubscription,
JetStreamSubscriptionInfoable,
JetStreamSubscriptionOptions,
JsMsgCallback,
KV,
KvCodec,
KvCodecs,
KvEntry,
KvLimits,
KvOptions,
KvPutOptions,
KvStatus,
KvWatchInclude,
KvWatchOptions,
ObjectInfo,
ObjectResult,
ObjectStore,
ObjectStoreLink,
ObjectStoreMeta,
ObjectStoreMetaOptions,
ObjectStoreOptions,
ObjectStorePutOpts,
ObjectStoreStatus,
PubAck,
Pullable,
RoKV,
StoredMsg,
Stream,
StreamAPI,
Streams,
Views,
} from "./types.ts";
export type { StreamNames } from "./jsbaseclient_api.ts";
export type {
AccountLimits,
ApiPagedRequest,
ClusterInfo,
ConsumerConfig,
ConsumerInfo,
ConsumerUpdateConfig,
ExternalStream,
JetStreamAccountStats,
JetStreamApiStats,
JetStreamUsageAccountLimits,
LastForMsgRequest,
LostStreamData,
MsgDeleteRequest,
MsgRequest,
PeerInfo,
Placement,
PullOptions,
PurgeBySeq,
PurgeBySubject,
PurgeOpts,
PurgeResponse,
PurgeTrimOpts,
Republish,
SeqMsgRequest,
SequenceInfo,
StreamAlternate,
StreamConfig,
StreamConsumerLimits,
StreamInfo,
StreamSource,
StreamSourceInfo,
StreamState,
StreamUpdateConfig,
SubjectTransformConfig,
} from "./jsapi_types.ts";
export type { JsMsg } from "./jsmsg.ts";
export type { Lister } from "./jslister.ts";
export {
AckPolicy,
DeliverPolicy,
DiscardPolicy,
ReplayPolicy,
RetentionPolicy,
StorageType,
StoreCompression,
} from "./jsapi_types.ts";
export type { ConsumerAPI } from "./jsmconsumer_api.ts";
export type { DeliveryInfo, StreamInfoRequestOptions } from "./jsapi_types.ts";
export type {
ConsumeBytes,
ConsumeCallback,
ConsumeMessages,
ConsumeOptions,
Consumer,
ConsumerCallbackFn,
ConsumerMessages,
ConsumerStatus,
Expires,
FetchBytes,
FetchMessages,
FetchOptions,
IdleHeartbeat,
MaxBytes,
MaxMessages,
OrderedConsumerOptions,
ThresholdBytes,
ThresholdMessages,
} from "./consumer.ts";
export { ConsumerDebugEvents, ConsumerEvents } from "./consumer.ts";

File diff suppressed because it is too large Load Diff

@ -0,0 +1,128 @@
/*
* Copyright 2021-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Empty } from "../nats-base-client/encoders.ts";
import { Codec, JSONCodec } from "../nats-base-client/codec.ts";
import { extend } from "../nats-base-client/util.ts";
import { NatsConnectionImpl } from "../nats-base-client/nats.ts";
import { checkJsErrorCode } from "./jsutil.ts";
import {
JetStreamOptions,
Msg,
NatsConnection,
RequestOptions,
} from "../nats-base-client/core.ts";
import { ApiResponse } from "./jsapi_types.ts";
const defaultPrefix = "$JS.API";
const defaultTimeout = 5000;
export function defaultJsOptions(opts?: JetStreamOptions): JetStreamOptions {
opts = opts || {} as JetStreamOptions;
if (opts.domain) {
opts.apiPrefix = `$JS.${opts.domain}.API`;
delete opts.domain;
}
return extend({ apiPrefix: defaultPrefix, timeout: defaultTimeout }, opts);
}
export interface StreamNames {
streams: string[];
}
export interface StreamNameBySubject {
subject: string;
}
export class BaseApiClient {
nc: NatsConnectionImpl;
opts: JetStreamOptions;
prefix: string;
timeout: number;
jc: Codec<unknown>;
constructor(nc: NatsConnection, opts?: JetStreamOptions) {
this.nc = nc as NatsConnectionImpl;
this.opts = defaultJsOptions(opts);
this._parseOpts();
this.prefix = this.opts.apiPrefix!;
this.timeout = this.opts.timeout!;
this.jc = JSONCodec();
}
getOptions(): JetStreamOptions {
return Object.assign({}, this.opts);
}
_parseOpts() {
let prefix = this.opts.apiPrefix;
if (!prefix || prefix.length === 0) {
throw new Error("invalid empty prefix");
}
const c = prefix[prefix.length - 1];
if (c === ".") {
prefix = prefix.substr(0, prefix.length - 1);
}
this.opts.apiPrefix = prefix;
}
async _request(
subj: string,
data: unknown = null,
opts?: RequestOptions,
): Promise<unknown> {
opts = opts || {} as RequestOptions;
opts.timeout = this.timeout;
let a: Uint8Array = Empty;
if (data) {
a = this.jc.encode(data);
}
const m = await this.nc.request(
subj,
a,
opts,
);
return this.parseJsResponse(m);
}
async findStream(subject: string): Promise<string> {
const q = { subject } as StreamNameBySubject;
const r = await this._request(`${this.prefix}.STREAM.NAMES`, q);
const names = r as StreamNames;
if (!names.streams || names.streams.length !== 1) {
throw new Error("no stream matches subject");
}
return names.streams[0];
}
getConnection(): NatsConnection {
return this.nc;
}
parseJsResponse(m: Msg): unknown {
const v = this.jc.decode(m.data);
const r = v as ApiResponse;
if (r.error) {
const err = checkJsErrorCode(r.error.code, r.error.description);
if (err !== null) {
err.api_error = r.error;
throw err;
}
}
return v;
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,117 @@
/*
* Copyright 2021-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BaseApiClient } from "./jsbaseclient_api.ts";
import {
ApiPaged,
ApiPagedRequest,
ApiResponse,
ConsumerListResponse,
StreamListResponse,
} from "./jsapi_types.ts";
/**
* An interface for listing. Returns a promise with typed list.
*/
export interface Lister<T> {
[Symbol.asyncIterator](): AsyncIterator<T>;
next(): Promise<T[]>;
}
export type ListerFieldFilter<T> = (v: unknown) => T[];
export class ListerImpl<T> implements Lister<T>, AsyncIterable<T> {
err?: Error;
offset: number;
pageInfo: ApiPaged;
subject: string;
jsm: BaseApiClient;
filter: ListerFieldFilter<T>;
payload: unknown;
constructor(
subject: string,
filter: ListerFieldFilter<T>,
jsm: BaseApiClient,
payload?: unknown,
) {
if (!subject) {
throw new Error("subject is required");
}
this.subject = subject;
this.jsm = jsm;
this.offset = 0;
this.pageInfo = {} as ApiPaged;
this.filter = filter;
this.payload = payload || {};
}
async next(): Promise<T[]> {
if (this.err) {
return [];
}
if (this.pageInfo && this.offset >= this.pageInfo.total) {
return [];
}
const offset = { offset: this.offset } as ApiPagedRequest;
if (this.payload) {
Object.assign(offset, this.payload);
}
try {
const r = await this.jsm._request(
this.subject,
offset,
{ timeout: this.jsm.timeout },
);
this.pageInfo = r as ApiPaged;
// offsets are reported in total, so need to count
// all the entries returned
this.offset += this.countResponse(r as ApiResponse);
const a = this.filter(r);
return a;
} catch (err) {
this.err = err;
throw err;
}
}
countResponse(r?: ApiResponse): number {
switch (r?.type) {
case "io.nats.jetstream.api.v1.stream_names_response":
case "io.nats.jetstream.api.v1.stream_list_response":
return (r as StreamListResponse).streams.length;
case "io.nats.jetstream.api.v1.consumer_list_response":
return (r as ConsumerListResponse).consumers.length;
default:
console.error(
`jslister.ts: unknown API response for paged output: ${r?.type}`,
);
// has to be a stream...
return (r as StreamListResponse).streams?.length || 0;
}
return 0;
}
async *[Symbol.asyncIterator]() {
let page = await this.next();
while (page.length > 0) {
for (const item of page) {
yield item;
}
page = await this.next();
}
}
}

@ -0,0 +1,173 @@
/*
* Copyright 2021-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BaseApiClient } from "./jsbaseclient_api.ts";
import { StreamAPIImpl } from "./jsmstream_api.ts";
import { ConsumerAPI, ConsumerAPIImpl } from "./jsmconsumer_api.ts";
import { QueuedIteratorImpl } from "../nats-base-client/queued_iterator.ts";
import {
Advisory,
AdvisoryKind,
DirectMsg,
DirectMsgHeaders,
DirectStreamAPI,
JetStreamClient,
JetStreamManager,
StoredMsg,
StreamAPI,
} from "./types.ts";
import {
JetStreamOptions,
Msg,
MsgHdrs,
NatsConnection,
ReviverFn,
} from "../nats-base-client/core.ts";
import {
AccountInfoResponse,
ApiResponse,
DirectMsgRequest,
JetStreamAccountStats,
LastForMsgRequest,
} from "./jsapi_types.ts";
import { checkJsError, validateStreamName } from "./jsutil.ts";
import { Empty, TD } from "../nats-base-client/encoders.ts";
import { Codec, JSONCodec } from "../nats-base-client/codec.ts";
export class DirectStreamAPIImpl extends BaseApiClient
implements DirectStreamAPI {
constructor(nc: NatsConnection, opts?: JetStreamOptions) {
super(nc, opts);
}
async getMessage(
stream: string,
query: DirectMsgRequest,
): Promise<StoredMsg> {
validateStreamName(stream);
// if doing a last_by_subj request, we append the subject
// this allows last_by_subj to be subject to permissions (KV)
let qq: DirectMsgRequest | null = query;
const { last_by_subj } = qq as LastForMsgRequest;
if (last_by_subj) {
qq = null;
}
const payload = qq ? this.jc.encode(qq) : Empty;
const pre = this.opts.apiPrefix || "$JS.API";
const subj = last_by_subj
? `${pre}.DIRECT.GET.${stream}.${last_by_subj}`
: `${pre}.DIRECT.GET.${stream}`;
const r = await this.nc.request(
subj,
payload,
);
// response is not a JS.API response
const err = checkJsError(r);
if (err) {
return Promise.reject(err);
}
const dm = new DirectMsgImpl(r);
return Promise.resolve(dm);
}
}
export class DirectMsgImpl implements DirectMsg {
data: Uint8Array;
header: MsgHdrs;
static jc?: Codec<unknown>;
constructor(m: Msg) {
if (!m.headers) {
throw new Error("headers expected");
}
this.data = m.data;
this.header = m.headers;
}
get subject(): string {
return this.header.get(DirectMsgHeaders.Subject);
}
get seq(): number {
const v = this.header.get(DirectMsgHeaders.Sequence);
return typeof v === "string" ? parseInt(v) : 0;
}
get time(): Date {
return new Date(Date.parse(this.timestamp));
}
get timestamp(): string {
return this.header.get(DirectMsgHeaders.TimeStamp);
}
get stream(): string {
return this.header.get(DirectMsgHeaders.Stream);
}
json<T = unknown>(reviver?: ReviverFn): T {
return JSONCodec<T>(reviver).decode(this.data);
}
string(): string {
return TD.decode(this.data);
}
}
export class JetStreamManagerImpl extends BaseApiClient
implements JetStreamManager {
streams: StreamAPI;
consumers: ConsumerAPI;
direct: DirectStreamAPI;
constructor(nc: NatsConnection, opts?: JetStreamOptions) {
super(nc, opts);
this.streams = new StreamAPIImpl(nc, opts);
this.consumers = new ConsumerAPIImpl(nc, opts);
this.direct = new DirectStreamAPIImpl(nc, opts);
}
async getAccountInfo(): Promise<JetStreamAccountStats> {
const r = await this._request(`${this.prefix}.INFO`);
return r as AccountInfoResponse;
}
jetstream(): JetStreamClient {
return this.nc.jetstream(this.getOptions());
}
advisories(): AsyncIterable<Advisory> {
const iter = new QueuedIteratorImpl<Advisory>();
this.nc.subscribe(`$JS.EVENT.ADVISORY.>`, {
callback: (err, msg) => {
if (err) {
throw err;
}
try {
const d = this.parseJsResponse(msg) as ApiResponse;
const chunks = d.type.split(".");
const kind = chunks[chunks.length - 1];
iter.push({ kind: kind as AdvisoryKind, data: d });
} catch (err) {
iter.stop(err);
}
},
});
return iter;
}
}

@ -0,0 +1,215 @@
/*
* Copyright 2021-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BaseApiClient } from "./jsbaseclient_api.ts";
import { Lister, ListerFieldFilter, ListerImpl } from "./jslister.ts";
import {
minValidation,
validateDurableName,
validateStreamName,
} from "./jsutil.ts";
import { NatsConnectionImpl } from "../nats-base-client/nats.ts";
import { Feature } from "../nats-base-client/semver.ts";
import { JetStreamOptions, NatsConnection } from "../nats-base-client/core.ts";
import {
ConsumerApiAction,
ConsumerConfig,
ConsumerInfo,
ConsumerListResponse,
ConsumerUpdateConfig,
CreateConsumerRequest,
SuccessResponse,
} from "./jsapi_types.ts";
export interface ConsumerAPI {
/**
* Returns the ConsumerInfo for the specified consumer in the specified stream.
* @param stream
* @param consumer
*/
info(stream: string, consumer: string): Promise<ConsumerInfo>;
/**
* Adds a new consumer to the specified stream with the specified consumer options.
* @param stream
* @param cfg
*/
add(stream: string, cfg: Partial<ConsumerConfig>): Promise<ConsumerInfo>;
/**
* Updates the consumer configuration for the specified consumer on the specified
* stream that has the specified durable name.
* @param stream
* @param durable
* @param cfg
*/
update(
stream: string,
durable: string,
cfg: Partial<ConsumerUpdateConfig>,
): Promise<ConsumerInfo>;
/**
* Deletes the specified consumer name/durable from the specified stream.
* @param stream
* @param consumer
*/
delete(stream: string, consumer: string): Promise<boolean>;
/**
* Lists all the consumers on the specfied streams
* @param stream
*/
list(stream: string): Lister<ConsumerInfo>;
}
export class ConsumerAPIImpl extends BaseApiClient implements ConsumerAPI {
constructor(nc: NatsConnection, opts?: JetStreamOptions) {
super(nc, opts);
}
async add(
stream: string,
cfg: ConsumerConfig,
action = ConsumerApiAction.Create,
): Promise<ConsumerInfo> {
validateStreamName(stream);
if (cfg.deliver_group && cfg.flow_control) {
throw new Error(
"jetstream flow control is not supported with queue groups",
);
}
if (cfg.deliver_group && cfg.idle_heartbeat) {
throw new Error(
"jetstream idle heartbeat is not supported with queue groups",
);
}
const cr = {} as CreateConsumerRequest;
cr.config = cfg;
cr.stream_name = stream;
cr.action = action;
if (cr.config.durable_name) {
validateDurableName(cr.config.durable_name);
}
const nci = this.nc as NatsConnectionImpl;
let { min, ok: newAPI } = nci.features.get(
Feature.JS_NEW_CONSUMER_CREATE_API,
);
const name = cfg.name === "" ? undefined : cfg.name;
if (name && !newAPI) {
throw new Error(`consumer 'name' requires server ${min}`);
}
if (name) {
try {
minValidation("name", name);
} catch (err) {
// if we have a cannot contain the message, massage a bit
const m = err.message;
const idx = m.indexOf("cannot contain");
if (idx !== -1) {
throw new Error(`consumer 'name' ${m.substring(idx)}`);
}
throw err;
}
}
let subj;
let consumerName = "";
// new api doesn't support multiple filter subjects
// this delayed until here because the consumer in an update could have
// been created with the new API, and have a `name`
if (Array.isArray(cfg.filter_subjects)) {
const { min, ok } = nci.features.get(Feature.JS_MULTIPLE_CONSUMER_FILTER);
if (!ok) {
throw new Error(`consumer 'filter_subjects' requires server ${min}`);
}
newAPI = false;
}
if (cfg.metadata) {
const { min, ok } = nci.features.get(Feature.JS_STREAM_CONSUMER_METADATA);
if (!ok) {
throw new Error(`consumer 'metadata' requires server ${min}`);
}
}
if (newAPI) {
consumerName = cfg.name ?? cfg.durable_name ?? "";
}
if (consumerName !== "") {
let fs = cfg.filter_subject ?? undefined;
if (fs === ">") {
fs = undefined;
}
subj = fs !== undefined
? `${this.prefix}.CONSUMER.CREATE.${stream}.${consumerName}.${fs}`
: `${this.prefix}.CONSUMER.CREATE.${stream}.${consumerName}`;
} else {
subj = cfg.durable_name
? `${this.prefix}.CONSUMER.DURABLE.CREATE.${stream}.${cfg.durable_name}`
: `${this.prefix}.CONSUMER.CREATE.${stream}`;
}
const r = await this._request(subj, cr);
return r as ConsumerInfo;
}
async update(
stream: string,
durable: string,
cfg: ConsumerUpdateConfig,
): Promise<ConsumerInfo> {
const ci = await this.info(stream, durable);
const changable = cfg as ConsumerConfig;
return this.add(
stream,
Object.assign(ci.config, changable),
ConsumerApiAction.Update,
);
}
async info(stream: string, name: string): Promise<ConsumerInfo> {
validateStreamName(stream);
validateDurableName(name);
const r = await this._request(
`${this.prefix}.CONSUMER.INFO.${stream}.${name}`,
);
return r as ConsumerInfo;
}
async delete(stream: string, name: string): Promise<boolean> {
validateStreamName(stream);
validateDurableName(name);
const r = await this._request(
`${this.prefix}.CONSUMER.DELETE.${stream}.${name}`,
);
const cr = r as SuccessResponse;
return cr.success;
}
list(stream: string): Lister<ConsumerInfo> {
validateStreamName(stream);
const filter: ListerFieldFilter<ConsumerInfo> = (
v: unknown,
): ConsumerInfo[] => {
const clr = v as ConsumerListResponse;
return clr.consumers;
};
const subj = `${this.prefix}.CONSUMER.LIST.${stream}`;
return new ListerImpl<ConsumerInfo>(subj, filter, this);
}
}

@ -0,0 +1,292 @@
/*
* Copyright 2021-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DataBuffer } from "../nats-base-client/databuffer.ts";
import { JSONCodec, StringCodec } from "../nats-base-client/codec.ts";
import { MsgImpl } from "../nats-base-client/msg.ts";
import { ProtocolHandler } from "../nats-base-client/protocol.ts";
import { RequestOne } from "../nats-base-client/request.ts";
import { nanos } from "./jsutil.ts";
import { Msg, MsgHdrs, RequestOptions } from "../nats-base-client/core.ts";
import { DeliveryInfo, PullOptions } from "./jsapi_types.ts";
export const ACK = Uint8Array.of(43, 65, 67, 75);
const NAK = Uint8Array.of(45, 78, 65, 75);
const WPI = Uint8Array.of(43, 87, 80, 73);
const NXT = Uint8Array.of(43, 78, 88, 84);
const TERM = Uint8Array.of(43, 84, 69, 82, 77);
const SPACE = Uint8Array.of(32);
/**
* Represents a message stored in JetStream
*/
export interface JsMsg {
/**
* True if the message was redelivered
*/
redelivered: boolean;
/**
* The delivery info for the message
*/
info: DeliveryInfo;
/**
* The sequence number for the message
*/
seq: number;
/**
* Any headers associated with the message
*/
headers: MsgHdrs | undefined;
/**
* The message's data
*/
data: Uint8Array;
/**
* The subject on which the message was published
*/
subject: string;
/**
* @ignore
*/
sid: number;
/**
* Indicate to the JetStream server that the message was processed
* successfully.
*/
ack(): void;
/**
* Indicate to the JetStream server that processing of the message
* failed, and that it should be resent after the spefied number of
* milliseconds.
* @param millis
*/
nak(millis?: number): void;
/**
* Indicate to the JetStream server that processing of the message
* is on going, and that the ack wait timer for the message should be
* reset preventing a redelivery.
*/
working(): void;
/**
* !! this is an experimental feature - and could be removed
*
* next() combines ack() and pull(), requires the subject for a
* subscription processing to process a message is provided
* (can be the same) however, because the ability to specify
* how long to keep the request open can be specified, this
* functionality doesn't work well with iterators, as an error
* (408s) are expected and needed to re-trigger a pull in case
* there was a timeout. In an iterator, the error will close
* the iterator, requiring a subscription to be reset.
*/
next(subj: string, ro?: Partial<PullOptions>): void;
/**
* Indicate to the JetStream server that processing of the message
* failed and that the message should not be sent to the consumer again.
*/
term(): void;
/**
* Indicate to the JetStream server that the message was processed
* successfully and that the JetStream server should acknowledge back
* that the acknowledgement was received.
*/
ackAck(): Promise<boolean>;
/**
* Convenience method to parse the message payload as JSON. This method
* will throw an exception if there's a parsing error;
*/
json<T>(): T;
/**
* Convenience method to parse the message payload as string. This method
* may throw an exception if there's a conversion error
*/
string(): string;
}
export function toJsMsg(m: Msg): JsMsg {
return new JsMsgImpl(m);
}
export function parseInfo(s: string): DeliveryInfo {
const tokens = s.split(".");
if (tokens.length === 9) {
tokens.splice(2, 0, "_", "");
}
if (
(tokens.length < 11) || tokens[0] !== "$JS" || tokens[1] !== "ACK"
) {
throw new Error(`not js message`);
}
// old
// "$JS.ACK.<stream>.<consumer>.<redeliveryCount><streamSeq><deliverySequence>.<timestamp>.<pending>"
// new
// $JS.ACK.<domain>.<accounthash>.<stream>.<consumer>.<redeliveryCount>.<streamSeq>.<deliverySequence>.<timestamp>.<pending>.<random>
const di = {} as DeliveryInfo;
// if domain is "_", replace with blank
di.domain = tokens[2] === "_" ? "" : tokens[2];
di.account_hash = tokens[3];
di.stream = tokens[4];
di.consumer = tokens[5];
di.redeliveryCount = parseInt(tokens[6], 10);
di.redelivered = di.redeliveryCount > 1;
di.streamSequence = parseInt(tokens[7], 10);
di.deliverySequence = parseInt(tokens[8], 10);
di.timestampNanos = parseInt(tokens[9], 10);
di.pending = parseInt(tokens[10], 10);
return di;
}
export class JsMsgImpl implements JsMsg {
msg: Msg;
di?: DeliveryInfo;
didAck: boolean;
constructor(msg: Msg) {
this.msg = msg;
this.didAck = false;
}
get subject(): string {
return this.msg.subject;
}
get sid(): number {
return this.msg.sid;
}
get data(): Uint8Array {
return this.msg.data;
}
get headers(): MsgHdrs {
return this.msg.headers!;
}
get info(): DeliveryInfo {
if (!this.di) {
this.di = parseInfo(this.reply);
}
return this.di;
}
get redelivered(): boolean {
return this.info.redeliveryCount > 1;
}
get reply(): string {
return this.msg.reply || "";
}
get seq(): number {
return this.info.streamSequence;
}
doAck(payload: Uint8Array) {
if (!this.didAck) {
// all acks are final with the exception of +WPI
this.didAck = !this.isWIP(payload);
this.msg.respond(payload);
}
}
isWIP(p: Uint8Array) {
return p.length === 4 && p[0] === WPI[0] && p[1] === WPI[1] &&
p[2] === WPI[2] && p[3] === WPI[3];
}
// this has to dig into the internals as the message has access
// to the protocol but not the high-level client.
async ackAck(): Promise<boolean> {
if (!this.didAck) {
this.didAck = true;
if (this.msg.reply) {
const mi = this.msg as MsgImpl;
const proto = mi.publisher as unknown as ProtocolHandler;
const r = new RequestOne(proto.muxSubscriptions, this.msg.reply);
proto.request(r);
try {
proto.publish(
this.msg.reply,
ACK,
{
reply: `${proto.muxSubscriptions.baseInbox}${r.token}`,
},
);
} catch (err) {
r.cancel(err);
}
try {
await Promise.race([r.timer, r.deferred]);
return true;
} catch (err) {
r.cancel(err);
}
}
}
return false;
}
ack() {
this.doAck(ACK);
}
nak(millis?: number) {
let payload = NAK;
if (millis) {
payload = StringCodec().encode(
`-NAK ${JSON.stringify({ delay: nanos(millis) })}`,
);
}
this.doAck(payload);
}
working() {
this.doAck(WPI);
}
next(subj: string, opts: Partial<PullOptions> = { batch: 1 }) {
const args: Partial<PullOptions> = {};
args.batch = opts.batch || 1;
args.no_wait = opts.no_wait || false;
if (opts.expires && opts.expires > 0) {
args.expires = nanos(opts.expires);
}
const data = JSONCodec().encode(args);
const payload = DataBuffer.concat(NXT, SPACE, data);
const reqOpts = subj ? { reply: subj } as RequestOptions : undefined;
this.msg.respond(payload, reqOpts);
}
term() {
this.doAck(TERM);
}
json<T = unknown>(): T {
return this.msg.json();
}
string(): string {
return this.msg.string();
}
}

@ -0,0 +1,592 @@
/*
* Copyright 2021-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Empty, MsgHdrs } from "../nats-base-client/types.ts";
import { BaseApiClient, StreamNames } from "./jsbaseclient_api.ts";
import { Lister, ListerFieldFilter, ListerImpl } from "./jslister.ts";
import { validateStreamName } from "./jsutil.ts";
import { headers, MsgHdrsImpl } from "../nats-base-client/headers.ts";
import { KvStatusImpl } from "./kv.ts";
import { ObjectStoreStatusImpl, osPrefix } from "./objectstore.ts";
import { Codec, JSONCodec } from "../nats-base-client/codec.ts";
import { TD } from "../nats-base-client/encoders.ts";
import { Feature } from "../nats-base-client/semver.ts";
import { NatsConnectionImpl } from "../nats-base-client/nats.ts";
import {
Consumers,
kvPrefix,
KvStatus,
ObjectStoreStatus,
StoredMsg,
Stream,
StreamAPI,
Streams,
} from "./types.ts";
import {
JetStreamOptions,
NatsConnection,
ReviverFn,
} from "../nats-base-client/core.ts";
import {
ApiPagedRequest,
ExternalStream,
MsgDeleteRequest,
MsgRequest,
PurgeBySeq,
PurgeOpts,
PurgeResponse,
PurgeTrimOpts,
StreamAlternate,
StreamConfig,
StreamInfo,
StreamInfoRequestOptions,
StreamListResponse,
StreamMsgResponse,
StreamSource,
StreamUpdateConfig,
SuccessResponse,
} from "./jsapi_types.ts";
import {
Consumer,
OrderedConsumerOptions,
OrderedPullConsumerImpl,
PullConsumerImpl,
} from "./consumer.ts";
import { ConsumerAPI, ConsumerAPIImpl } from "./jsmconsumer_api.ts";
export function convertStreamSourceDomain(s?: StreamSource) {
if (s === undefined) {
return undefined;
}
const { domain } = s;
if (domain === undefined) {
return s;
}
const copy = Object.assign({}, s) as StreamSource;
delete copy.domain;
if (domain === "") {
return copy;
}
if (copy.external) {
throw new Error("domain and external are both set");
}
copy.external = { api: `$JS.${domain}.API` } as ExternalStream;
return copy;
}
export class ConsumersImpl implements Consumers {
api: ConsumerAPI;
notified: boolean;
constructor(api: ConsumerAPI) {
this.api = api;
this.notified = false;
}
checkVersion(): Promise<void> {
const fv = (this.api as ConsumerAPIImpl).nc.features.get(
Feature.JS_SIMPLIFICATION,
);
if (!fv.ok) {
return Promise.reject(
new Error(
`consumers framework is only supported on servers ${fv.min} or better`,
),
);
}
return Promise.resolve();
}
async get(
stream: string,
name: string | Partial<OrderedConsumerOptions> = {},
): Promise<Consumer> {
if (typeof name === "object") {
return this.ordered(stream, name);
}
// check we have support for pending msgs and header notifications
await this.checkVersion();
return this.api.info(stream, name)
.then((ci) => {
if (ci.config.deliver_subject !== undefined) {
return Promise.reject(new Error("push consumer not supported"));
}
return new PullConsumerImpl(this.api, ci);
})
.catch((err) => {
return Promise.reject(err);
});
}
async ordered(
stream: string,
opts?: Partial<OrderedConsumerOptions>,
): Promise<Consumer> {
await this.checkVersion();
const impl = this.api as ConsumerAPIImpl;
const sapi = new StreamAPIImpl(impl.nc, impl.opts);
return sapi.info(stream)
.then((_si) => {
return Promise.resolve(
new OrderedPullConsumerImpl(this.api, stream, opts),
);
})
.catch((err) => {
return Promise.reject(err);
});
}
}
export class StreamImpl implements Stream {
api: StreamAPIImpl;
_info: StreamInfo;
constructor(api: StreamAPI, info: StreamInfo) {
this.api = api as StreamAPIImpl;
this._info = info;
}
get name(): string {
return this._info.config.name;
}
alternates(): Promise<StreamAlternate[]> {
return this.info()
.then((si) => {
return si.alternates ? si.alternates : [];
});
}
async best(): Promise<Stream> {
await this.info();
if (this._info.alternates) {
const asi = await this.api.info(this._info.alternates[0].name);
return new StreamImpl(this.api, asi);
} else {
return this;
}
}
info(
cached = false,
opts?: Partial<StreamInfoRequestOptions>,
): Promise<StreamInfo> {
if (cached) {
return Promise.resolve(this._info);
}
return this.api.info(this.name, opts)
.then((si) => {
this._info = si;
return this._info;
});
}
getConsumer(
name?: string | Partial<OrderedConsumerOptions>,
): Promise<Consumer> {
return new ConsumersImpl(new ConsumerAPIImpl(this.api.nc, this.api.opts))
.get(this.name, name);
}
getMessage(query: MsgRequest): Promise<StoredMsg> {
return this.api.getMessage(this.name, query);
}
deleteMessage(seq: number, erase?: boolean): Promise<boolean> {
return this.api.deleteMessage(this.name, seq, erase);
}
}
export class StreamAPIImpl extends BaseApiClient implements StreamAPI {
constructor(nc: NatsConnection, opts?: JetStreamOptions) {
super(nc, opts);
}
checkStreamConfigVersions(cfg: Partial<StreamConfig>) {
const nci = this.nc as NatsConnectionImpl;
if (cfg.metadata) {
const { min, ok } = nci.features.get(Feature.JS_STREAM_CONSUMER_METADATA);
if (!ok) {
throw new Error(`stream 'metadata' requires server ${min}`);
}
}
if (cfg.first_seq) {
const { min, ok } = nci.features.get(Feature.JS_STREAM_FIRST_SEQ);
if (!ok) {
throw new Error(`stream 'first_seq' requires server ${min}`);
}
}
if (cfg.subject_transform) {
const { min, ok } = nci.features.get(Feature.JS_STREAM_SUBJECT_TRANSFORM);
if (!ok) {
throw new Error(`stream 'subject_transform' requires server ${min}`);
}
}
if (cfg.compression) {
const { min, ok } = nci.features.get(Feature.JS_STREAM_COMPRESSION);
if (!ok) {
throw new Error(`stream 'compression' requires server ${min}`);
}
}
if (cfg.consumer_limits) {
const { min, ok } = nci.features.get(Feature.JS_DEFAULT_CONSUMER_LIMITS);
if (!ok) {
throw new Error(`stream 'consumer_limits' requires server ${min}`);
}
}
function validateStreamSource(
context: string,
src: Partial<StreamSource>,
): void {
const count = src.subject_transforms?.length || 0;
if (count > 0) {
const { min, ok } = nci.features.get(
Feature.JS_STREAM_SOURCE_SUBJECT_TRANSFORM,
);
if (!ok) {
throw new Error(
`${context} 'subject_transforms' requires server ${min}`,
);
}
}
}
if (cfg.sources) {
cfg.sources.forEach((src) => {
validateStreamSource("stream sources", src);
});
}
if (cfg.mirror) {
validateStreamSource("stream mirror", cfg.mirror);
}
}
async add(cfg = {} as Partial<StreamConfig>): Promise<StreamInfo> {
this.checkStreamConfigVersions(cfg);
validateStreamName(cfg.name);
cfg.mirror = convertStreamSourceDomain(cfg.mirror);
//@ts-ignore: the sources are either set or not - so no item should be undefined in the list
cfg.sources = cfg.sources?.map(convertStreamSourceDomain);
const r = await this._request(
`${this.prefix}.STREAM.CREATE.${cfg.name}`,
cfg,
);
const si = r as StreamInfo;
this._fixInfo(si);
return si;
}
async delete(stream: string): Promise<boolean> {
validateStreamName(stream);
const r = await this._request(`${this.prefix}.STREAM.DELETE.${stream}`);
const cr = r as SuccessResponse;
return cr.success;
}
async update(
name: string,
cfg = {} as Partial<StreamUpdateConfig>,
): Promise<StreamInfo> {
if (typeof name === "object") {
const sc = name as StreamConfig;
name = sc.name;
cfg = sc;
console.trace(
`\u001B[33m >> streams.update(config: StreamConfig) api changed to streams.update(name: string, config: StreamUpdateConfig) - this shim will be removed - update your code. \u001B[0m`,
);
}
this.checkStreamConfigVersions(cfg);
validateStreamName(name);
const old = await this.info(name);
const update = Object.assign(old.config, cfg);
update.mirror = convertStreamSourceDomain(update.mirror);
//@ts-ignore: the sources are either set or not - so no item should be undefined in the list
update.sources = update.sources?.map(convertStreamSourceDomain);
const r = await this._request(
`${this.prefix}.STREAM.UPDATE.${name}`,
update,
);
const si = r as StreamInfo;
this._fixInfo(si);
return si;
}
async info(
name: string,
data?: Partial<StreamInfoRequestOptions>,
): Promise<StreamInfo> {
validateStreamName(name);
const subj = `${this.prefix}.STREAM.INFO.${name}`;
const r = await this._request(subj, data);
let si = r as StreamInfo;
let { total, limit } = si;
// check how many subjects we got in the first request
let have = si.state.subjects
? Object.getOwnPropertyNames(si.state.subjects).length
: 1;
// if the response is paged, we have a large list of subjects
// handle the paging and return a StreamInfo with all of it
if (total && total > have) {
const infos: StreamInfo[] = [si];
const paged = data || {} as unknown as ApiPagedRequest;
let i = 0;
// total could change, so it is possible to have collected
// more that the total
while (total > have) {
i++;
paged.offset = limit * i;
const r = await this._request(subj, paged) as StreamInfo;
// update it in case it changed
total = r.total;
infos.push(r);
const count = Object.getOwnPropertyNames(r.state.subjects).length;
have += count;
// if request returns less than limit it is done
if (count < limit) {
// done
break;
}
}
// collect all the subjects
let subjects = {};
for (let i = 0; i < infos.length; i++) {
si = infos[i];
if (si.state.subjects) {
subjects = Object.assign(subjects, si.state.subjects);
}
}
// don't give the impression we paged
si.offset = 0;
si.total = 0;
si.limit = 0;
si.state.subjects = subjects;
}
this._fixInfo(si);
return si;
}
list(subject = ""): Lister<StreamInfo> {
const payload = subject?.length ? { subject } : {};
const listerFilter: ListerFieldFilter<StreamInfo> = (
v: unknown,
): StreamInfo[] => {
const slr = v as StreamListResponse;
slr.streams.forEach((si) => {
this._fixInfo(si);
});
return slr.streams;
};
const subj = `${this.prefix}.STREAM.LIST`;
return new ListerImpl<StreamInfo>(subj, listerFilter, this, payload);
}
// FIXME: init of sealed, deny_delete, deny_purge shouldn't be necessary
// https://github.com/nats-io/nats-server/issues/2633
_fixInfo(si: StreamInfo) {
si.config.sealed = si.config.sealed || false;
si.config.deny_delete = si.config.deny_delete || false;
si.config.deny_purge = si.config.deny_purge || false;
si.config.allow_rollup_hdrs = si.config.allow_rollup_hdrs || false;
}
async purge(name: string, opts?: PurgeOpts): Promise<PurgeResponse> {
if (opts) {
const { keep, seq } = opts as PurgeBySeq & PurgeTrimOpts;
if (typeof keep === "number" && typeof seq === "number") {
throw new Error("can specify one of keep or seq");
}
}
validateStreamName(name);
const v = await this._request(`${this.prefix}.STREAM.PURGE.${name}`, opts);
return v as PurgeResponse;
}
async deleteMessage(
stream: string,
seq: number,
erase = true,
): Promise<boolean> {
validateStreamName(stream);
const dr = { seq } as MsgDeleteRequest;
if (!erase) {
dr.no_erase = true;
}
const r = await this._request(
`${this.prefix}.STREAM.MSG.DELETE.${stream}`,
dr,
);
const cr = r as SuccessResponse;
return cr.success;
}
async getMessage(stream: string, query: MsgRequest): Promise<StoredMsg> {
validateStreamName(stream);
const r = await this._request(
`${this.prefix}.STREAM.MSG.GET.${stream}`,
query,
);
const sm = r as StreamMsgResponse;
return new StoredMsgImpl(sm);
}
find(subject: string): Promise<string> {
return this.findStream(subject);
}
listKvs(): Lister<KvStatus> {
const filter: ListerFieldFilter<KvStatus> = (
v: unknown,
): KvStatus[] => {
const slr = v as StreamListResponse;
const kvStreams = slr.streams.filter((v) => {
return v.config.name.startsWith(kvPrefix);
});
kvStreams.forEach((si) => {
this._fixInfo(si);
});
let cluster = "";
if (kvStreams.length) {
cluster = this.nc.info?.cluster ?? "";
}
const status = kvStreams.map((si) => {
return new KvStatusImpl(si, cluster);
});
return status;
};
const subj = `${this.prefix}.STREAM.LIST`;
return new ListerImpl<KvStatus>(subj, filter, this);
}
listObjectStores(): Lister<ObjectStoreStatus> {
const filter: ListerFieldFilter<ObjectStoreStatus> = (
v: unknown,
): ObjectStoreStatus[] => {
const slr = v as StreamListResponse;
const objStreams = slr.streams.filter((v) => {
return v.config.name.startsWith(osPrefix);
});
objStreams.forEach((si) => {
this._fixInfo(si);
});
const status = objStreams.map((si) => {
return new ObjectStoreStatusImpl(si);
});
return status;
};
const subj = `${this.prefix}.STREAM.LIST`;
return new ListerImpl<ObjectStoreStatus>(subj, filter, this);
}
names(subject = ""): Lister<string> {
const payload = subject?.length ? { subject } : {};
const listerFilter: ListerFieldFilter<string> = (
v: unknown,
): string[] => {
const sr = v as StreamNames;
return sr.streams;
};
const subj = `${this.prefix}.STREAM.NAMES`;
return new ListerImpl<string>(subj, listerFilter, this, payload);
}
async get(name: string): Promise<Stream> {
const si = await this.info(name);
return Promise.resolve(new StreamImpl(this, si));
}
}
export class StoredMsgImpl implements StoredMsg {
_header?: MsgHdrs;
smr: StreamMsgResponse;
static jc?: Codec<unknown>;
constructor(smr: StreamMsgResponse) {
this.smr = smr;
}
get subject(): string {
return this.smr.message.subject;
}
get seq(): number {
return this.smr.message.seq;
}
get timestamp(): string {
return this.smr.message.time;
}
get time(): Date {
return new Date(Date.parse(this.timestamp));
}
get data(): Uint8Array {
return this.smr.message.data ? this._parse(this.smr.message.data) : Empty;
}
get header(): MsgHdrs {
if (!this._header) {
if (this.smr.message.hdrs) {
const hd = this._parse(this.smr.message.hdrs);
this._header = MsgHdrsImpl.decode(hd);
} else {
this._header = headers();
}
}
return this._header;
}
_parse(s: string): Uint8Array {
const bs = atob(s);
const len = bs.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = bs.charCodeAt(i);
}
return bytes;
}
json<T = unknown>(reviver?: ReviverFn): T {
return JSONCodec<T>(reviver).decode(this.data);
}
string(): string {
return TD.decode(this.data);
}
}
export class StreamsImpl implements Streams {
api: StreamAPIImpl;
constructor(api: StreamAPI) {
this.api = api as StreamAPIImpl;
}
get(stream: string): Promise<Stream> {
return this.api.info(stream)
.then((si) => {
return new StreamImpl(this.api, si);
});
}
}

@ -0,0 +1,236 @@
/*
* Copyright 2021-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Empty } from "../nats-base-client/encoders.ts";
import { MsgArg } from "../nats-base-client/parser.ts";
import { headers, MsgHdrsImpl } from "../nats-base-client/headers.ts";
import { MsgImpl } from "../nats-base-client/msg.ts";
import {
ErrorCode,
Msg,
Nanos,
NatsError,
Publisher,
} from "../nats-base-client/core.ts";
export function validateDurableName(name?: string) {
return minValidation("durable", name);
}
export function validateStreamName(name?: string) {
return minValidation("stream", name);
}
export function minValidation(context: string, name = "") {
// minimum validation on streams/consumers matches nats cli
if (name === "") {
throw Error(`${context} name required`);
}
const bad = [".", "*", ">", "/", "\\", " ", "\t", "\n", "\r"];
bad.forEach((v) => {
if (name.indexOf(v) !== -1) {
// make the error have a meaningful character
switch (v) {
case "\n":
v = "\\n";
break;
case "\r":
v = "\\r";
break;
case "\t":
v = "\\t";
break;
default:
// nothing
}
throw Error(
`invalid ${context} name - ${context} name cannot contain '${v}'`,
);
}
});
return "";
}
export function validateName(context: string, name = "") {
if (name === "") {
throw Error(`${context} name required`);
}
const m = validName(name);
if (m.length) {
throw new Error(`invalid ${context} name - ${context} name ${m}`);
}
}
export function validName(name = ""): string {
if (name === "") {
throw Error(`name required`);
}
const RE = /^[-\w]+$/g;
const m = name.match(RE);
if (m === null) {
for (const c of name.split("")) {
const mm = c.match(RE);
if (mm === null) {
return `cannot contain '${c}'`;
}
}
}
return "";
}
/**
* Converts the specified millis into Nanos
* @param millis
*/
export function nanos(millis: number): Nanos {
return millis * 1000000;
}
/**
* Convert the specified Nanos into millis
* @param ns
*/
export function millis(ns: Nanos) {
return Math.floor(ns / 1000000);
}
/**
* Returns true if the message is a flow control message
* @param msg
*/
export function isFlowControlMsg(msg: Msg): boolean {
if (msg.data.length > 0) {
return false;
}
const h = msg.headers;
if (!h) {
return false;
}
return h.code >= 100 && h.code < 200;
}
/**
* Returns true if the message is a heart beat message
* @param msg
*/
export function isHeartbeatMsg(msg: Msg): boolean {
return isFlowControlMsg(msg) && msg.headers?.description === "Idle Heartbeat";
}
export function newJsErrorMsg(
code: number,
description: string,
subject: string,
): Msg {
const h = headers(code, description) as MsgHdrsImpl;
const arg = { hdr: 1, sid: 0, size: 0 } as MsgArg;
const msg = new MsgImpl(arg, Empty, {} as Publisher);
msg._headers = h;
msg._subject = subject;
return msg;
}
export function checkJsError(msg: Msg): NatsError | null {
// JS error only if no payload - otherwise assume it is application data
if (msg.data.length !== 0) {
return null;
}
const h = msg.headers;
if (!h) {
return null;
}
return checkJsErrorCode(h.code, h.description);
}
export enum Js409Errors {
MaxBatchExceeded = "exceeded maxrequestbatch of",
MaxExpiresExceeded = "exceeded maxrequestexpires of",
MaxBytesExceeded = "exceeded maxrequestmaxbytes of",
MaxMessageSizeExceeded = "message size exceeds maxbytes",
PushConsumer = "consumer is push based",
MaxWaitingExceeded = "exceeded maxwaiting", // not terminal
IdleHeartbeatMissed = "idle heartbeats missed",
ConsumerDeleted = "consumer deleted",
// FIXME: consumer deleted - instead of no responder (terminal error)
// leadership changed -
}
let MAX_WAITING_FAIL = false;
export function setMaxWaitingToFail(tf: boolean) {
MAX_WAITING_FAIL = tf;
}
export function isTerminal409(err: NatsError): boolean {
if (err.code !== ErrorCode.JetStream409) {
return false;
}
const fatal = [
Js409Errors.MaxBatchExceeded,
Js409Errors.MaxExpiresExceeded,
Js409Errors.MaxBytesExceeded,
Js409Errors.MaxMessageSizeExceeded,
Js409Errors.PushConsumer,
Js409Errors.IdleHeartbeatMissed,
Js409Errors.ConsumerDeleted,
];
if (MAX_WAITING_FAIL) {
fatal.push(Js409Errors.MaxWaitingExceeded);
}
return fatal.find((s) => {
return err.message.indexOf(s) !== -1;
}) !== undefined;
}
export function checkJsErrorCode(
code: number,
description = "",
): NatsError | null {
if (code < 300) {
return null;
}
description = description.toLowerCase();
switch (code) {
case 404:
// 404 for jetstream will provide different messages ensure we
// keep whatever the server returned
return new NatsError(description, ErrorCode.JetStream404NoMessages);
case 408:
return new NatsError(description, ErrorCode.JetStream408RequestTimeout);
case 409: {
// the description can be exceeded max waiting or max ack pending, which are
// recoverable, but can also be terminal errors where the request exceeds
// some value in the consumer configuration
const ec = description.startsWith(Js409Errors.IdleHeartbeatMissed)
? ErrorCode.JetStreamIdleHeartBeat
: ErrorCode.JetStream409;
return new NatsError(
description,
ec,
);
}
case 503:
return NatsError.errorForCode(
ErrorCode.JetStreamNotEnabled,
new Error(description),
);
default:
if (description === "") {
description = ErrorCode.Unknown;
}
return new NatsError(description, `${code}`);
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,142 @@
/*
* Copyright 2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
checkJsError,
isFlowControlMsg,
isHeartbeatMsg,
millis,
nanos,
} from "./internal_mod.ts";
export {
AckPolicy,
AdvisoryKind,
ConsumerDebugEvents,
ConsumerEvents,
DeliverPolicy,
DirectMsgHeaders,
DiscardPolicy,
JsHeaders,
ReplayPolicy,
RepublishHeaders,
RetentionPolicy,
StorageType,
StoreCompression,
} from "./internal_mod.ts";
export type {
AccountLimits,
Advisory,
ApiPagedRequest,
Closed,
ClusterInfo,
ConsumeBytes,
ConsumeCallback,
ConsumeMessages,
ConsumeOptions,
Consumer,
ConsumerAPI,
ConsumerCallbackFn,
ConsumerConfig,
ConsumerInfo,
ConsumerInfoable,
ConsumerMessages,
ConsumerOpts,
ConsumerOptsBuilder,
Consumers,
ConsumerStatus,
ConsumerUpdateConfig,
DeliveryInfo,
Destroyable,
Expires,
ExternalStream,
FetchBytes,
FetchMessages,
FetchOptions,
IdleHeartbeat,
JetStreamAccountStats,
JetStreamApiStats,
JetStreamClient,
JetStreamManager,
JetStreamManagerOptions,
JetStreamOptions,
JetStreamPublishOptions,
JetStreamPullSubscription,
JetStreamSubscription,
JetStreamSubscriptionOptions,
JetStreamUsageAccountLimits,
JsMsg,
JsMsgCallback,
KV,
KvCodec,
KvCodecs,
KvEntry,
KvLimits,
KvOptions,
KvPutOptions,
KvStatus,
KvWatchInclude,
KvWatchOptions,
LastForMsgRequest,
Lister,
LostStreamData,
MaxBytes,
MaxMessages,
MsgDeleteRequest,
MsgRequest,
ObjectInfo,
ObjectResult,
ObjectStore,
ObjectStoreLink,
ObjectStoreMeta,
ObjectStoreMetaOptions,
ObjectStoreOptions,
ObjectStorePutOpts,
ObjectStoreStatus,
OrderedConsumerOptions,
PeerInfo,
Placement,
PubAck,
Pullable,
PullOptions,
PurgeBySeq,
PurgeBySubject,
PurgeOpts,
PurgeResponse,
PurgeTrimOpts,
Republish,
RoKV,
SeqMsgRequest,
SequenceInfo,
StoredMsg,
Stream,
StreamAlternate,
StreamAPI,
StreamConfig,
StreamConsumerLimits,
StreamInfo,
StreamInfoRequestOptions,
StreamNames,
Streams,
StreamSource,
StreamSourceInfo,
StreamState,
StreamUpdateConfig,
SubjectTransformConfig,
ThresholdBytes,
ThresholdMessages,
Views,
} from "./internal_mod.ts";
export { consumerOpts } from "./types.ts";

@ -0,0 +1,812 @@
/*
* Copyright 2022-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { validateBucket } from "./kv.ts";
import { Base64UrlPaddedCodec } from "../nats-base-client/base64.ts";
import { JSONCodec } from "../nats-base-client/codec.ts";
import { nuid } from "../nats-base-client/nuid.ts";
import { deferred } from "../nats-base-client/util.ts";
import { DataBuffer } from "../nats-base-client/databuffer.ts";
import { headers, MsgHdrsImpl } from "../nats-base-client/headers.ts";
import {
consumerOpts,
JetStreamClient,
JetStreamManager,
JsHeaders,
ObjectInfo,
ObjectResult,
ObjectStore,
ObjectStoreMeta,
ObjectStoreMetaOptions,
ObjectStoreOptions,
ObjectStorePutOpts,
ObjectStoreStatus,
PubAck,
} from "./types.ts";
import { QueuedIteratorImpl } from "../nats-base-client/queued_iterator.ts";
import { SHA256 } from "../nats-base-client/sha256.js";
import {
MsgHdrs,
NatsConnection,
NatsError,
QueuedIterator,
} from "../nats-base-client/core.ts";
import {
DiscardPolicy,
PurgeResponse,
StorageType,
StreamConfig,
StreamInfo,
StreamInfoRequestOptions,
} from "./jsapi_types.ts";
import { JsMsg } from "./jsmsg.ts";
import { PubHeaders } from "./jsclient.ts";
export const osPrefix = "OBJ_";
export const digestType = "SHA-256=";
export function objectStoreStreamName(bucket: string): string {
validateBucket(bucket);
return `${osPrefix}${bucket}`;
}
export function objectStoreBucketName(stream: string): string {
if (stream.startsWith(osPrefix)) {
return stream.substring(4);
}
return stream;
}
export class ObjectStoreStatusImpl implements ObjectStoreStatus {
si: StreamInfo;
backingStore: string;
constructor(si: StreamInfo) {
this.si = si;
this.backingStore = "JetStream";
}
get bucket(): string {
return objectStoreBucketName(this.si.config.name);
}
get description(): string {
return this.si.config.description ?? "";
}
get ttl(): number {
return this.si.config.max_age;
}
get storage(): StorageType {
return this.si.config.storage;
}
get replicas(): number {
return this.si.config.num_replicas;
}
get sealed(): boolean {
return this.si.config.sealed;
}
get size(): number {
return this.si.state.bytes;
}
get streamInfo(): StreamInfo {
return this.si;
}
get metadata(): Record<string, string> | undefined {
return this.si.config.metadata;
}
}
export type ServerObjectStoreMeta = {
name: string;
description?: string;
headers?: Record<string, string[]>;
options?: ObjectStoreMetaOptions;
};
export type ServerObjectInfo = {
bucket: string;
nuid: string;
size: number;
chunks: number;
digest: string;
deleted?: boolean;
mtime: string;
revision: number;
metadata?: Record<string, string>;
} & ServerObjectStoreMeta;
class ObjectInfoImpl implements ObjectInfo {
info: ServerObjectInfo;
hdrs!: MsgHdrs;
constructor(oi: ServerObjectInfo) {
this.info = oi;
}
get name(): string {
return this.info.name;
}
get description(): string {
return this.info.description ?? "";
}
get headers(): MsgHdrs {
if (!this.hdrs) {
this.hdrs = MsgHdrsImpl.fromRecord(this.info.headers || {});
}
return this.hdrs;
}
get options(): ObjectStoreMetaOptions | undefined {
return this.info.options;
}
get bucket(): string {
return this.info.bucket;
}
get chunks(): number {
return this.info.chunks;
}
get deleted(): boolean {
return this.info.deleted ?? false;
}
get digest(): string {
return this.info.digest;
}
get mtime(): string {
return this.info.mtime;
}
get nuid(): string {
return this.info.nuid;
}
get size(): number {
return this.info.size;
}
get revision(): number {
return this.info.revision;
}
get metadata(): Record<string, string> {
return this.info.metadata || {};
}
isLink() {
return (this.info.options?.link !== undefined) &&
(this.info.options?.link !== null);
}
}
function toServerObjectStoreMeta(
meta: Partial<ObjectStoreMeta>,
): ServerObjectStoreMeta {
const v = {
name: meta.name,
description: meta.description ?? "",
options: meta.options,
metadata: meta.metadata,
} as ServerObjectStoreMeta;
if (meta.headers) {
const mhi = meta.headers as MsgHdrsImpl;
v.headers = mhi.toRecord();
}
return v;
}
function emptyReadableStream(): ReadableStream {
return new ReadableStream({
pull(c) {
c.enqueue(new Uint8Array(0));
c.close();
},
});
}
export class ObjectStoreImpl implements ObjectStore {
jsm: JetStreamManager;
js: JetStreamClient;
stream!: string;
name: string;
constructor(name: string, jsm: JetStreamManager, js: JetStreamClient) {
this.name = name;
this.jsm = jsm;
this.js = js;
}
_checkNotEmpty(name: string): { name: string; error?: Error } {
if (!name || name.length === 0) {
return { name, error: new Error("name cannot be empty") };
}
return { name };
}
async info(name: string): Promise<ObjectInfo | null> {
const info = await this.rawInfo(name);
return info ? new ObjectInfoImpl(info) : null;
}
async list(): Promise<ObjectInfo[]> {
const buf: ObjectInfo[] = [];
const iter = await this.watch({
ignoreDeletes: true,
includeHistory: true,
});
for await (const info of iter) {
// watch will give a null when it has initialized
// for us that is the hint we are done
if (info === null) {
break;
}
buf.push(info);
}
return Promise.resolve(buf);
}
async rawInfo(name: string): Promise<ServerObjectInfo | null> {
const { name: obj, error } = this._checkNotEmpty(name);
if (error) {
return Promise.reject(error);
}
const meta = this._metaSubject(obj);
try {
const m = await this.jsm.streams.getMessage(this.stream, {
last_by_subj: meta,
});
const jc = JSONCodec<ServerObjectInfo>();
const soi = jc.decode(m.data) as ServerObjectInfo;
soi.revision = m.seq;
return soi;
} catch (err) {
if (err.code === "404") {
return null;
}
return Promise.reject(err);
}
}
async _si(
opts?: Partial<StreamInfoRequestOptions>,
): Promise<StreamInfo | null> {
try {
return await this.jsm.streams.info(this.stream, opts);
} catch (err) {
const nerr = err as NatsError;
if (nerr.code === "404") {
return null;
}
return Promise.reject(err);
}
}
async seal(): Promise<ObjectStoreStatus> {
let info = await this._si();
if (info === null) {
return Promise.reject(new Error("object store not found"));
}
info.config.sealed = true;
info = await this.jsm.streams.update(this.stream, info.config);
return Promise.resolve(new ObjectStoreStatusImpl(info));
}
async status(
opts?: Partial<StreamInfoRequestOptions>,
): Promise<ObjectStoreStatus> {
const info = await this._si(opts);
if (info === null) {
return Promise.reject(new Error("object store not found"));
}
return Promise.resolve(new ObjectStoreStatusImpl(info));
}
destroy(): Promise<boolean> {
return this.jsm.streams.delete(this.stream);
}
async _put(
meta: ObjectStoreMeta,
rs: ReadableStream<Uint8Array> | null,
opts?: ObjectStorePutOpts,
): Promise<ObjectInfo> {
const jsopts = this.js.getOptions();
opts = opts || { timeout: jsopts.timeout };
opts.timeout = opts.timeout || jsopts.timeout;
opts.previousRevision = opts.previousRevision ?? undefined;
const { timeout, previousRevision } = opts;
const si = (this.js as unknown as { nc: NatsConnection }).nc.info;
const maxPayload = si?.max_payload || 1024;
meta = meta || {} as ObjectStoreMeta;
meta.options = meta.options || {};
let maxChunk = meta.options?.max_chunk_size || 128 * 1024;
maxChunk = maxChunk > maxPayload ? maxPayload : maxChunk;
meta.options.max_chunk_size = maxChunk;
const old = await this.info(meta.name);
const { name: n, error } = this._checkNotEmpty(meta.name);
if (error) {
return Promise.reject(error);
}
const id = nuid.next();
const chunkSubj = this._chunkSubject(id);
const metaSubj = this._metaSubject(n);
const info = Object.assign({
bucket: this.name,
nuid: id,
size: 0,
chunks: 0,
}, toServerObjectStoreMeta(meta)) as ServerObjectInfo;
const d = deferred<ObjectInfo>();
const proms: Promise<unknown>[] = [];
const db = new DataBuffer();
try {
const reader = rs ? rs.getReader() : null;
const sha = new SHA256();
while (true) {
const { done, value } = reader
? await reader.read()
: { done: true, value: undefined };
if (done) {
// put any partial chunk in
if (db.size() > 0) {
const payload = db.drain();
sha.update(payload);
info.chunks!++;
info.size! += payload.length;
proms.push(this.js.publish(chunkSubj, payload, { timeout }));
}
// wait for all the chunks to write
await Promise.all(proms);
proms.length = 0;
// prepare the metadata
info.mtime = new Date().toISOString();
const digest = sha.digest("base64");
const pad = digest.length % 3;
const padding = pad > 0 ? "=".repeat(pad) : "";
info.digest = `${digestType}${digest}${padding}`;
info.deleted = false;
// trailing md for the object
const h = headers();
if (typeof previousRevision === "number") {
h.set(
PubHeaders.ExpectedLastSubjectSequenceHdr,
`${previousRevision}`,
);
}
h.set(JsHeaders.RollupHdr, JsHeaders.RollupValueSubject);
// try to update the metadata
const pa = await this.js.publish(metaSubj, JSONCodec().encode(info), {
headers: h,
timeout,
});
// update the revision to point to the sequence where we inserted
info.revision = pa.seq;
// if we are here, the new entry is live
if (old) {
try {
await this.jsm.streams.purge(this.stream, {
filter: `$O.${this.name}.C.${old.nuid}`,
});
} catch (_err) {
// rejecting here, would mean send the wrong signal
// the update succeeded, but cleanup of old chunks failed.
}
}
// resolve the ObjectInfo
d.resolve(new ObjectInfoImpl(info!));
// stop
break;
}
if (value) {
db.fill(value);
while (db.size() > maxChunk) {
info.chunks!++;
info.size! += maxChunk;
const payload = db.drain(meta.options.max_chunk_size);
sha.update(payload);
proms.push(
this.js.publish(chunkSubj, payload, { timeout }),
);
}
}
}
} catch (err) {
// we failed, remove any partials
await this.jsm.streams.purge(this.stream, { filter: chunkSubj });
d.reject(err);
}
return d;
}
putBlob(
meta: ObjectStoreMeta,
data: Uint8Array | null,
opts?: ObjectStorePutOpts,
): Promise<ObjectInfo> {
function readableStreamFrom(data: Uint8Array): ReadableStream<Uint8Array> {
return new ReadableStream<Uint8Array>({
pull(controller) {
controller.enqueue(data);
controller.close();
},
});
}
if (data === null) {
data = new Uint8Array(0);
}
return this.put(meta, readableStreamFrom(data), opts);
}
put(
meta: ObjectStoreMeta,
rs: ReadableStream<Uint8Array> | null,
opts?: ObjectStorePutOpts,
): Promise<ObjectInfo> {
if (meta?.options?.link) {
return Promise.reject(
new Error("link cannot be set when putting the object in bucket"),
);
}
return this._put(meta, rs, opts);
}
async getBlob(name: string): Promise<Uint8Array | null> {
async function fromReadableStream(
rs: ReadableStream<Uint8Array>,
): Promise<Uint8Array> {
const buf = new DataBuffer();
const reader = rs.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
return buf.drain();
}
if (value && value.length) {
buf.fill(value);
}
}
}
const r = await this.get(name);
if (r === null) {
return Promise.resolve(null);
}
const vs = await Promise.all([r.error, fromReadableStream(r.data)]);
if (vs[0]) {
return Promise.reject(vs[0]);
} else {
return Promise.resolve(vs[1]);
}
}
async get(name: string): Promise<ObjectResult | null> {
const info = await this.rawInfo(name);
if (info === null) {
return Promise.resolve(null);
}
if (info.deleted) {
return Promise.resolve(null);
}
if (info.options && info.options.link) {
const ln = info.options.link.name || "";
if (ln === "") {
throw new Error("link is a bucket");
}
const os = info.options.link.bucket !== this.name
? await ObjectStoreImpl.create(
this.js,
info.options.link.bucket,
)
: this;
return os.get(ln);
}
const d = deferred<Error | null>();
const r: Partial<ObjectResult> = {
info: new ObjectInfoImpl(info),
error: d,
};
if (info.size === 0) {
r.data = emptyReadableStream();
d.resolve(null);
return Promise.resolve(r as ObjectResult);
}
let controller: ReadableStreamDefaultController;
const oc = consumerOpts();
oc.orderedConsumer();
const sha = new SHA256();
const subj = `$O.${this.name}.C.${info.nuid}`;
const sub = await this.js.subscribe(subj, oc);
(async () => {
for await (const jm of sub) {
if (jm.data.length > 0) {
sha.update(jm.data);
controller!.enqueue(jm.data);
}
if (jm.info.pending === 0) {
const hash = sha.digest("base64");
// go pads the hash - which should be multiple of 3 - otherwise pads with '='
const pad = hash.length % 3;
const padding = pad > 0 ? "=".repeat(pad) : "";
const digest = `${digestType}${hash}${padding}`;
if (digest !== info.digest) {
controller!.error(
new Error(
`received a corrupt object, digests do not match received: ${info.digest} calculated ${digest}`,
),
);
} else {
controller!.close();
}
sub.unsubscribe();
}
}
})()
.then(() => {
d.resolve();
})
.catch((err) => {
controller!.error(err);
d.reject(err);
});
r.data = new ReadableStream({
start(c) {
controller = c;
},
cancel() {
sub.unsubscribe();
},
});
return r as ObjectResult;
}
linkStore(name: string, bucket: ObjectStore): Promise<ObjectInfo> {
if (!(bucket instanceof ObjectStoreImpl)) {
return Promise.reject("bucket required");
}
const osi = bucket as ObjectStoreImpl;
const { name: n, error } = this._checkNotEmpty(name);
if (error) {
return Promise.reject(error);
}
const meta = {
name: n,
options: { link: { bucket: osi.name } },
};
return this._put(meta, null);
}
async link(name: string, info: ObjectInfo): Promise<ObjectInfo> {
const { name: n, error } = this._checkNotEmpty(name);
if (error) {
return Promise.reject(error);
}
if (info.deleted) {
return Promise.reject(new Error("src object is deleted"));
}
if ((info as ObjectInfoImpl).isLink()) {
return Promise.reject(new Error("src object is a link"));
}
const dest = await this.rawInfo(name);
if (dest !== null && !dest.deleted) {
return Promise.reject(
new Error("an object already exists with that name"),
);
}
const link = { bucket: info.bucket, name: info.name };
const mm = {
name: n,
bucket: info.bucket,
options: { link: link },
} as ObjectStoreMeta;
await this.js.publish(this._metaSubject(name), JSON.stringify(mm));
const i = await this.info(name);
return Promise.resolve(i!);
}
async delete(name: string): Promise<PurgeResponse> {
const info = await this.rawInfo(name);
if (info === null) {
return Promise.resolve({ purged: 0, success: false });
}
info.deleted = true;
info.size = 0;
info.chunks = 0;
info.digest = "";
const jc = JSONCodec();
const h = headers();
h.set(JsHeaders.RollupHdr, JsHeaders.RollupValueSubject);
await this.js.publish(this._metaSubject(info.name), jc.encode(info), {
headers: h,
});
return this.jsm.streams.purge(this.stream, {
filter: this._chunkSubject(info.nuid),
});
}
async update(
name: string,
meta: Partial<ObjectStoreMeta> = {},
): Promise<PubAck> {
const info = await this.rawInfo(name);
if (info === null) {
return Promise.reject(new Error("object not found"));
}
if (info.deleted) {
return Promise.reject(
new Error("cannot update meta for a deleted object"),
);
}
meta.name = meta.name ?? info.name;
const { name: n, error } = this._checkNotEmpty(meta.name);
if (error) {
return Promise.reject(error);
}
if (name !== meta.name) {
const i = await this.info(meta.name);
if (i && !i.deleted) {
return Promise.reject(
new Error("an object already exists with that name"),
);
}
}
meta.name = n;
const ii = Object.assign({}, info, toServerObjectStoreMeta(meta!));
// if the name changed, delete the old meta
const ack = await this.js.publish(
this._metaSubject(ii.name),
JSON.stringify(ii),
);
if (name !== meta.name) {
await this.jsm.streams.purge(this.stream, {
filter: this._metaSubject(name),
});
}
return Promise.resolve(ack);
}
async watch(opts: Partial<
{
ignoreDeletes?: boolean;
includeHistory?: boolean;
}
> = {}): Promise<QueuedIterator<ObjectInfo | null>> {
opts.includeHistory = opts.includeHistory ?? false;
opts.ignoreDeletes = opts.ignoreDeletes ?? false;
let initialized = false;
const qi = new QueuedIteratorImpl<ObjectInfo | null>();
const subj = this._metaSubjectAll();
try {
await this.jsm.streams.getMessage(this.stream, { last_by_subj: subj });
} catch (err) {
if (err.code === "404") {
qi.push(null);
initialized = true;
} else {
qi.stop(err);
}
}
const jc = JSONCodec<ObjectInfo>();
const copts = consumerOpts();
copts.orderedConsumer();
if (opts.includeHistory) {
copts.deliverLastPerSubject();
} else {
// FIXME: Go's implementation doesn't seem correct - if history is not desired
// the watch should only be giving notifications on new entries
initialized = true;
copts.deliverNew();
}
copts.callback((err: NatsError | null, jm: JsMsg | null) => {
if (err) {
qi.stop(err);
return;
}
if (jm !== null) {
const oi = jc.decode(jm.data);
if (oi.deleted && opts.ignoreDeletes === true) {
// do nothing
} else {
qi.push(oi);
}
if (jm.info?.pending === 0 && !initialized) {
initialized = true;
qi.push(null);
}
}
});
const sub = await this.js.subscribe(subj, copts);
qi._data = sub;
qi.iterClosed.then(() => {
sub.unsubscribe();
});
sub.closed.then(() => {
qi.stop();
}).catch((err) => {
qi.stop(err);
});
return qi;
}
_chunkSubject(id: string) {
return `$O.${this.name}.C.${id}`;
}
_metaSubject(n: string): string {
return `$O.${this.name}.M.${Base64UrlPaddedCodec.encode(n)}`;
}
_metaSubjectAll(): string {
return `$O.${this.name}.M.>`;
}
async init(opts: Partial<ObjectStoreOptions> = {}): Promise<void> {
try {
this.stream = objectStoreStreamName(this.name);
} catch (err) {
return Promise.reject(err);
}
const max_age = opts?.ttl || 0;
delete opts.ttl;
const sc = Object.assign({ max_age }, opts) as StreamConfig;
sc.name = this.stream;
sc.allow_direct = true;
sc.allow_rollup_hdrs = true;
sc.discard = DiscardPolicy.New;
sc.subjects = [`$O.${this.name}.C.>`, `$O.${this.name}.M.>`];
if (opts.placement) {
sc.placement = opts.placement;
}
if (opts.metadata) {
sc.metadata = opts.metadata;
}
try {
await this.jsm.streams.info(sc.name);
} catch (err) {
if (err.message === "stream not found") {
await this.jsm.streams.add(sc);
}
}
}
static async create(
js: JetStreamClient,
name: string,
opts: Partial<ObjectStoreOptions> = {},
): Promise<ObjectStore> {
const jsm = await js.jetstreamManager();
const os = new ObjectStoreImpl(name, jsm, js);
await os.init(opts);
return Promise.resolve(os);
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,164 @@
/*
* Copyright 2020-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { nkeys } from "./nkeys.ts";
import { TD, TE } from "./encoders.ts";
import {
Auth,
Authenticator,
ErrorCode,
JwtAuth,
NatsError,
NKeyAuth,
NoAuth,
TokenAuth,
UserPass,
} from "./core.ts";
export function multiAuthenticator(authenticators: Authenticator[]) {
return (nonce?: string): Auth => {
let auth: Partial<NoAuth & TokenAuth & UserPass & NKeyAuth & JwtAuth> = {};
authenticators.forEach((a) => {
const args = a(nonce) || {};
auth = Object.assign(auth, args);
});
return auth as Auth;
};
}
export function noAuthFn(): Authenticator {
return (): NoAuth => {
return;
};
}
/**
* Returns a user/pass authenticator for the specified user and optional password
* @param { string | () => string } user
* @param {string | () => string } pass
* @return {UserPass}
*/
export function usernamePasswordAuthenticator(
user: string | (() => string),
pass?: string | (() => string),
): Authenticator {
return (): UserPass => {
const u = typeof user === "function" ? user() : user;
const p = typeof pass === "function" ? pass() : pass;
return { user: u, pass: p };
};
}
/**
* Returns a token authenticator for the specified token
* @param { string | () => string } token
* @return {TokenAuth}
*/
export function tokenAuthenticator(
token: string | (() => string),
): Authenticator {
return (): TokenAuth => {
const auth_token = typeof token === "function" ? token() : token;
return { auth_token };
};
}
/**
* Returns an Authenticator that returns a NKeyAuth based that uses the
* specified seed or function returning a seed.
* @param {Uint8Array | (() => Uint8Array)} seed - the nkey seed
* @return {NKeyAuth}
*/
export function nkeyAuthenticator(
seed?: Uint8Array | (() => Uint8Array),
): Authenticator {
return (nonce?: string): NKeyAuth => {
const s = typeof seed === "function" ? seed() : seed;
const kp = s ? nkeys.fromSeed(s) : undefined;
const nkey = kp ? kp.getPublicKey() : "";
const challenge = TE.encode(nonce || "");
const sigBytes = kp !== undefined && nonce ? kp.sign(challenge) : undefined;
const sig = sigBytes ? nkeys.encode(sigBytes) : "";
return { nkey, sig };
};
}
/**
* Returns an Authenticator function that returns a JwtAuth.
* If a seed is provided, the public key, and signature are
* calculated.
*
* @param {string | ()=>string} ajwt - the jwt
* @param {Uint8Array | ()=> Uint8Array } seed - the optional nkey seed
* @return {Authenticator}
*/
export function jwtAuthenticator(
ajwt: string | (() => string),
seed?: Uint8Array | (() => Uint8Array),
): Authenticator {
return (
nonce?: string,
): JwtAuth => {
const jwt = typeof ajwt === "function" ? ajwt() : ajwt;
const fn = nkeyAuthenticator(seed);
const { nkey, sig } = fn(nonce) as NKeyAuth;
return { jwt, nkey, sig };
};
}
/**
* Returns an Authenticator function that returns a JwtAuth.
* This is a convenience Authenticator that parses the
* specified creds and delegates to the jwtAuthenticator.
* @param {Uint8Array | () => Uint8Array } creds - the contents of a creds file or a function that returns the creds
* @returns {JwtAuth}
*/
export function credsAuthenticator(
creds: Uint8Array | (() => Uint8Array),
): Authenticator {
const fn = typeof creds !== "function" ? () => creds : creds;
const parse = () => {
const CREDS =
/\s*(?:(?:[-]{3,}[^\n]*[-]{3,}\n)(.+)(?:\n\s*[-]{3,}[^\n]*[-]{3,}\n))/ig;
const s = TD.decode(fn());
// get the JWT
let m = CREDS.exec(s);
if (!m) {
throw NatsError.errorForCode(ErrorCode.BadCreds);
}
const jwt = m[1].trim();
// get the nkey
m = CREDS.exec(s);
if (!m) {
throw NatsError.errorForCode(ErrorCode.BadCreds);
}
if (!m) {
throw NatsError.errorForCode(ErrorCode.BadCreds);
}
const seed = TE.encode(m[1].trim());
return { jwt, seed };
};
const jwtFn = () => {
const { jwt } = parse();
return jwt;
};
const nkeyFn = () => {
const { seed } = parse();
return seed;
};
return jwtAuthenticator(jwtFn, nkeyFn);
}

@ -0,0 +1,67 @@
export class Base64Codec {
static encode(bytes: string | Uint8Array): string {
if (typeof bytes === "string") {
return btoa(bytes);
}
const a = Array.from(bytes);
return btoa(String.fromCharCode(...a));
}
static decode(s: string, binary = false): Uint8Array | string {
const bin = atob(s);
if (!binary) {
return bin;
}
return Uint8Array.from(bin, (c) => c.charCodeAt(0));
}
}
export class Base64UrlCodec {
static encode(bytes: string | Uint8Array): string {
return Base64UrlCodec.toB64URLEncoding(Base64Codec.encode(bytes));
}
static decode(s: string, binary = false): Uint8Array | string {
return Base64Codec.decode(Base64UrlCodec.fromB64URLEncoding(s), binary);
}
static toB64URLEncoding(b64str: string): string {
return b64str
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
static fromB64URLEncoding(b64str: string): string {
// pads are % 4, but not necessary on decoding
return b64str
.replace(/_/g, "/")
.replace(/-/g, "+");
}
}
export class Base64UrlPaddedCodec {
static encode(bytes: string | Uint8Array): string {
return Base64UrlPaddedCodec.toB64URLEncoding(Base64Codec.encode(bytes));
}
static decode(s: string, binary = false): Uint8Array | string {
return Base64UrlPaddedCodec.decode(
Base64UrlPaddedCodec.fromB64URLEncoding(s),
binary,
);
}
static toB64URLEncoding(b64str: string): string {
return b64str
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
static fromB64URLEncoding(b64str: string): string {
// pads are % 4, but not necessary on decoding
return b64str
.replace(/_/g, "/")
.replace(/-/g, "+");
}
}

@ -0,0 +1,430 @@
/*
* Copyright 2020-2022 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Empty } from "./types.ts";
import { nuid } from "./nuid.ts";
import { deferred, Perf } from "./util.ts";
import type { NatsConnectionImpl } from "./nats.ts";
import { ErrorCode, NatsConnection, NatsError } from "./core.ts";
export class Metric {
name: string;
duration: number;
date: number;
payload: number;
msgs: number;
lang!: string;
version!: string;
bytes: number;
asyncRequests?: boolean;
min?: number;
max?: number;
constructor(name: string, duration: number) {
this.name = name;
this.duration = duration;
this.date = Date.now();
this.payload = 0;
this.msgs = 0;
this.bytes = 0;
}
toString(): string {
const sec = (this.duration) / 1000;
const mps = Math.round(this.msgs / sec);
const label = this.asyncRequests ? "asyncRequests" : "";
let minmax = "";
if (this.max) {
minmax = `${this.min}/${this.max}`;
}
return `${this.name}${label ? " [asyncRequests]" : ""} ${
humanizeNumber(mps)
} msgs/sec - [${sec.toFixed(2)} secs] ~ ${
throughput(this.bytes, sec)
} ${minmax}`;
}
toCsv(): string {
return `"${this.name}",${
new Date(this.date).toISOString()
},${this.lang},${this.version},${this.msgs},${this.payload},${this.bytes},${this.duration},${
this.asyncRequests ? this.asyncRequests : false
}\n`;
}
static header(): string {
return `Test,Date,Lang,Version,Count,MsgPayload,Bytes,Millis,Async\n`;
}
}
export interface BenchOpts {
callbacks?: boolean;
msgs?: number;
size?: number;
subject?: string;
asyncRequests?: boolean;
pub?: boolean;
sub?: boolean;
rep?: boolean;
req?: boolean;
}
export class Bench {
nc: NatsConnection;
callbacks: boolean;
msgs: number;
size: number;
subject: string;
asyncRequests?: boolean;
pub?: boolean;
sub?: boolean;
req?: boolean;
rep?: boolean;
perf: Perf;
payload: Uint8Array;
constructor(
nc: NatsConnection,
opts: BenchOpts = {
msgs: 100000,
size: 128,
subject: "",
asyncRequests: false,
pub: false,
sub: false,
req: false,
rep: false,
},
) {
this.nc = nc;
this.callbacks = opts.callbacks || false;
this.msgs = opts.msgs || 0;
this.size = opts.size || 0;
this.subject = opts.subject || nuid.next();
this.asyncRequests = opts.asyncRequests || false;
this.pub = opts.pub || false;
this.sub = opts.sub || false;
this.req = opts.req || false;
this.rep = opts.rep || false;
this.perf = new Perf();
this.payload = this.size ? new Uint8Array(this.size) : Empty;
if (!this.pub && !this.sub && !this.req && !this.rep) {
throw new Error("no bench option selected");
}
}
async run(): Promise<Metric[]> {
this.nc.closed()
.then((err) => {
if (err) {
throw new NatsError(
`bench closed with an error: ${err.message}`,
ErrorCode.Unknown,
err,
);
}
});
if (this.callbacks) {
await this.runCallbacks();
} else {
await this.runAsync();
}
return this.processMetrics();
}
processMetrics(): Metric[] {
const nc = this.nc as NatsConnectionImpl;
const { lang, version } = nc.protocol.transport;
if (this.pub && this.sub) {
this.perf.measure("pubsub", "pubStart", "subStop");
}
if (this.req && this.rep) {
this.perf.measure("reqrep", "reqStart", "reqStop");
}
const measures = this.perf.getEntries();
const pubsub = measures.find((m) => m.name === "pubsub");
const reqrep = measures.find((m) => m.name === "reqrep");
const req = measures.find((m) => m.name === "req");
const rep = measures.find((m) => m.name === "rep");
const pub = measures.find((m) => m.name === "pub");
const sub = measures.find((m) => m.name === "sub");
const stats = this.nc.stats();
const metrics: Metric[] = [];
if (pubsub) {
const { name, duration } = pubsub;
const m = new Metric(name, duration);
m.msgs = this.msgs * 2;
m.bytes = stats.inBytes + stats.outBytes;
m.lang = lang;
m.version = version;
m.payload = this.payload.length;
metrics.push(m);
}
if (reqrep) {
const { name, duration } = reqrep;
const m = new Metric(name, duration);
m.msgs = this.msgs * 2;
m.bytes = stats.inBytes + stats.outBytes;
m.lang = lang;
m.version = version;
m.payload = this.payload.length;
metrics.push(m);
}
if (pub) {
const { name, duration } = pub;
const m = new Metric(name, duration);
m.msgs = this.msgs;
m.bytes = stats.outBytes;
m.lang = lang;
m.version = version;
m.payload = this.payload.length;
metrics.push(m);
}
if (sub) {
const { name, duration } = sub;
const m = new Metric(name, duration);
m.msgs = this.msgs;
m.bytes = stats.inBytes;
m.lang = lang;
m.version = version;
m.payload = this.payload.length;
metrics.push(m);
}
if (rep) {
const { name, duration } = rep;
const m = new Metric(name, duration);
m.msgs = this.msgs;
m.bytes = stats.inBytes + stats.outBytes;
m.lang = lang;
m.version = version;
m.payload = this.payload.length;
metrics.push(m);
}
if (req) {
const { name, duration } = req;
const m = new Metric(name, duration);
m.msgs = this.msgs;
m.bytes = stats.inBytes + stats.outBytes;
m.lang = lang;
m.version = version;
m.payload = this.payload.length;
metrics.push(m);
}
return metrics;
}
async runCallbacks(): Promise<void> {
const jobs: Promise<void>[] = [];
if (this.sub) {
const d = deferred<void>();
jobs.push(d);
let i = 0;
this.nc.subscribe(this.subject, {
max: this.msgs,
callback: () => {
i++;
if (i === 1) {
this.perf.mark("subStart");
}
if (i === this.msgs) {
this.perf.mark("subStop");
this.perf.measure("sub", "subStart", "subStop");
d.resolve();
}
},
});
}
if (this.rep) {
const d = deferred<void>();
jobs.push(d);
let i = 0;
this.nc.subscribe(this.subject, {
max: this.msgs,
callback: (_, m) => {
m.respond(this.payload);
i++;
if (i === 1) {
this.perf.mark("repStart");
}
if (i === this.msgs) {
this.perf.mark("repStop");
this.perf.measure("rep", "repStart", "repStop");
d.resolve();
}
},
});
}
if (this.pub) {
const job = (async () => {
this.perf.mark("pubStart");
for (let i = 0; i < this.msgs; i++) {
this.nc.publish(this.subject, this.payload);
}
await this.nc.flush();
this.perf.mark("pubStop");
this.perf.measure("pub", "pubStart", "pubStop");
})();
jobs.push(job);
}
if (this.req) {
const job = (async () => {
if (this.asyncRequests) {
this.perf.mark("reqStart");
const a = [];
for (let i = 0; i < this.msgs; i++) {
a.push(
this.nc.request(this.subject, this.payload, { timeout: 20000 }),
);
}
await Promise.all(a);
this.perf.mark("reqStop");
this.perf.measure("req", "reqStart", "reqStop");
} else {
this.perf.mark("reqStart");
for (let i = 0; i < this.msgs; i++) {
await this.nc.request(this.subject);
}
this.perf.mark("reqStop");
this.perf.measure("req", "reqStart", "reqStop");
}
})();
jobs.push(job);
}
await Promise.all(jobs);
}
async runAsync(): Promise<void> {
const jobs: Promise<void>[] = [];
if (this.rep) {
let first = false;
const sub = this.nc.subscribe(this.subject, { max: this.msgs });
const job = (async () => {
for await (const m of sub) {
if (!first) {
this.perf.mark("repStart");
first = true;
}
m.respond(this.payload);
}
await this.nc.flush();
this.perf.mark("repStop");
this.perf.measure("rep", "repStart", "repStop");
})();
jobs.push(job);
}
if (this.sub) {
let first = false;
const sub = this.nc.subscribe(this.subject, { max: this.msgs });
const job = (async () => {
for await (const _m of sub) {
if (!first) {
this.perf.mark("subStart");
first = true;
}
}
this.perf.mark("subStop");
this.perf.measure("sub", "subStart", "subStop");
})();
jobs.push(job);
}
if (this.pub) {
const job = (async () => {
this.perf.mark("pubStart");
for (let i = 0; i < this.msgs; i++) {
this.nc.publish(this.subject, this.payload);
}
await this.nc.flush();
this.perf.mark("pubStop");
this.perf.measure("pub", "pubStart", "pubStop");
})();
jobs.push(job);
}
if (this.req) {
const job = (async () => {
if (this.asyncRequests) {
this.perf.mark("reqStart");
const a = [];
for (let i = 0; i < this.msgs; i++) {
a.push(
this.nc.request(this.subject, this.payload, { timeout: 20000 }),
);
}
await Promise.all(a);
this.perf.mark("reqStop");
this.perf.measure("req", "reqStart", "reqStop");
} else {
this.perf.mark("reqStart");
for (let i = 0; i < this.msgs; i++) {
await this.nc.request(this.subject);
}
this.perf.mark("reqStop");
this.perf.measure("req", "reqStart", "reqStop");
}
})();
jobs.push(job);
}
await Promise.all(jobs);
}
}
export function throughput(bytes: number, seconds: number): string {
return `${humanizeBytes(bytes / seconds)}/sec`;
}
export function msgThroughput(msgs: number, seconds: number): string {
return `${(Math.floor(msgs / seconds))} msgs/sec`;
}
export function humanizeBytes(bytes: number, si = false): string {
const base = si ? 1000 : 1024;
const pre = si
? ["k", "M", "G", "T", "P", "E"]
: ["K", "M", "G", "T", "P", "E"];
const post = si ? "iB" : "B";
if (bytes < base) {
return `${bytes.toFixed(2)} ${post}`;
}
const exp = parseInt(Math.log(bytes) / Math.log(base) + "");
const index = parseInt((exp - 1) + "");
return `${(bytes / Math.pow(base, exp)).toFixed(2)} ${pre[index]}${post}`;
}
function humanizeNumber(n: number) {
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

@ -0,0 +1,80 @@
/*
* Copyright 2020-2022 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TD, TE } from "./encoders.ts";
import { ErrorCode, NatsError } from "./core.ts";
export interface Codec<T> {
/**
* Encode T to an Uint8Array suitable for including in a message payload.
* @param d
*/
encode(d: T): Uint8Array;
/**
* Decode an Uint8Array from a message payload into a T
* @param a
*/
decode(a: Uint8Array): T;
}
/**
* Returns a {@link Codec} for encoding strings to a message payload
* and decoding message payloads into strings.
* @constructor
*/
export function StringCodec(): Codec<string> {
return {
encode(d: string): Uint8Array {
return TE.encode(d);
},
decode(a: Uint8Array): string {
return TD.decode(a);
},
};
}
/**
* Returns a {@link Codec} for encoding JavaScript object to JSON and
* serialize them to an Uint8Array, and conversely, from an
* Uint8Array to JSON to a JavaScript Object.
* @param reviver
* @constructor
*/
export function JSONCodec<T = unknown>(
reviver?: (this: unknown, key: string, value: unknown) => unknown,
): Codec<T> {
return {
encode(d: T): Uint8Array {
try {
if (d === undefined) {
// @ts-ignore: json will not handle undefined
d = null;
}
return TE.encode(JSON.stringify(d));
} catch (err) {
throw NatsError.errorForCode(ErrorCode.BadJson, err);
}
},
decode(a: Uint8Array): T {
try {
return JSON.parse(TD.decode(a), reviver);
} catch (err) {
throw NatsError.errorForCode(ErrorCode.BadJson, err);
}
},
};
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,129 @@
/*
* Copyright 2018-2021 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TD, TE } from "./encoders.ts";
export class DataBuffer {
buffers: Uint8Array[];
byteLength: number;
constructor() {
this.buffers = [];
this.byteLength = 0;
}
static concat(...bufs: Uint8Array[]): Uint8Array {
let max = 0;
for (let i = 0; i < bufs.length; i++) {
max += bufs[i].length;
}
const out = new Uint8Array(max);
let index = 0;
for (let i = 0; i < bufs.length; i++) {
out.set(bufs[i], index);
index += bufs[i].length;
}
return out;
}
static fromAscii(m: string): Uint8Array {
if (!m) {
m = "";
}
return TE.encode(m);
}
static toAscii(a: Uint8Array): string {
return TD.decode(a);
}
reset(): void {
this.buffers.length = 0;
this.byteLength = 0;
}
pack(): void {
if (this.buffers.length > 1) {
const v = new Uint8Array(this.byteLength);
let index = 0;
for (let i = 0; i < this.buffers.length; i++) {
v.set(this.buffers[i], index);
index += this.buffers[i].length;
}
this.buffers.length = 0;
this.buffers.push(v);
}
}
shift(): Uint8Array {
if (this.buffers.length) {
const a = this.buffers.shift();
if (a) {
this.byteLength -= a.length;
return a;
}
}
return new Uint8Array(0);
}
drain(n?: number): Uint8Array {
if (this.buffers.length) {
this.pack();
const v = this.buffers.pop();
if (v) {
const max = this.byteLength;
if (n === undefined || n > max) {
n = max;
}
const d = v.subarray(0, n);
if (max > n) {
this.buffers.push(v.subarray(n));
}
this.byteLength = max - n;
return d;
}
}
return new Uint8Array(0);
}
fill(a: Uint8Array, ...bufs: Uint8Array[]): void {
if (a) {
this.buffers.push(a);
this.byteLength += a.length;
}
for (let i = 0; i < bufs.length; i++) {
if (bufs[i] && bufs[i].length) {
this.buffers.push(bufs[i]);
this.byteLength += bufs[i].length;
}
}
}
peek(): Uint8Array {
if (this.buffers.length) {
this.pack();
return this.buffers[0];
}
return new Uint8Array(0);
}
size(): number {
return this.byteLength;
}
length(): number {
return this.buffers.length;
}
}

@ -0,0 +1,248 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
// This code has been ported almost directly from Go's src/bytes/buffer.go
// Copyright 2009 The Go Authors. All rights reserved. BSD license.
// https://github.com/golang/go/blob/master/LICENSE
// This code removes all Deno specific functionality to enable its use
// in a browser environment
//@internal
import { TE } from "./encoders.ts";
export class AssertionError extends Error {
constructor(msg?: string) {
super(msg);
this.name = "AssertionError";
}
}
export interface Reader {
read(p: Uint8Array): number | null;
}
export interface Writer {
write(p: Uint8Array): number;
}
// @internal
export function assert(cond: unknown, msg = "Assertion failed."): asserts cond {
if (!cond) {
throw new AssertionError(msg);
}
}
// MIN_READ is the minimum ArrayBuffer size passed to a read call by
// buffer.ReadFrom. As long as the Buffer has at least MIN_READ bytes beyond
// what is required to hold the contents of r, readFrom() will not grow the
// underlying buffer.
const MIN_READ = 32 * 1024;
export const MAX_SIZE = 2 ** 32 - 2;
// `off` is the offset into `dst` where it will at which to begin writing values
// from `src`.
// Returns the number of bytes copied.
function copy(src: Uint8Array, dst: Uint8Array, off = 0): number {
const r = dst.byteLength - off;
if (src.byteLength > r) {
src = src.subarray(0, r);
}
dst.set(src, off);
return src.byteLength;
}
export function concat(origin?: Uint8Array, b?: Uint8Array): Uint8Array {
if (origin === undefined && b === undefined) {
return new Uint8Array(0);
}
if (origin === undefined) {
return b!;
}
if (b === undefined) {
return origin;
}
const output = new Uint8Array(origin.length + b.length);
output.set(origin, 0);
output.set(b, origin.length);
return output;
}
export function append(origin: Uint8Array, b: number): Uint8Array {
return concat(origin, Uint8Array.of(b));
}
export class DenoBuffer implements Reader, Writer {
_buf: Uint8Array; // contents are the bytes _buf[off : len(_buf)]
_off: number; // read at _buf[off], write at _buf[_buf.byteLength]
constructor(ab?: ArrayBuffer) {
this._off = 0;
if (ab == null) {
this._buf = new Uint8Array(0);
return;
}
this._buf = new Uint8Array(ab);
}
bytes(options: { copy?: boolean } = { copy: true }): Uint8Array {
if (options.copy === false) return this._buf.subarray(this._off);
return this._buf.slice(this._off);
}
empty(): boolean {
return this._buf.byteLength <= this._off;
}
get length(): number {
return this._buf.byteLength - this._off;
}
get capacity(): number {
return this._buf.buffer.byteLength;
}
truncate(n: number): void {
if (n === 0) {
this.reset();
return;
}
if (n < 0 || n > this.length) {
throw Error("bytes.Buffer: truncation out of range");
}
this._reslice(this._off + n);
}
reset(): void {
this._reslice(0);
this._off = 0;
}
_tryGrowByReslice(n: number): number {
const l = this._buf.byteLength;
if (n <= this.capacity - l) {
this._reslice(l + n);
return l;
}
return -1;
}
_reslice(len: number): void {
assert(len <= this._buf.buffer.byteLength);
this._buf = new Uint8Array(this._buf.buffer, 0, len);
}
readByte(): number | null {
const a = new Uint8Array(1);
if (this.read(a)) {
return a[0];
}
return null;
}
read(p: Uint8Array): number | null {
if (this.empty()) {
// Buffer is empty, reset to recover space.
this.reset();
if (p.byteLength === 0) {
// this edge case is tested in 'bufferReadEmptyAtEOF' test
return 0;
}
return null;
}
const nread = copy(this._buf.subarray(this._off), p);
this._off += nread;
return nread;
}
writeByte(n: number): number {
return this.write(Uint8Array.of(n));
}
writeString(s: string): number {
return this.write(TE.encode(s));
}
write(p: Uint8Array): number {
const m = this._grow(p.byteLength);
return copy(p, this._buf, m);
}
_grow(n: number): number {
const m = this.length;
// If buffer is empty, reset to recover space.
if (m === 0 && this._off !== 0) {
this.reset();
}
// Fast: Try to _grow by means of a _reslice.
const i = this._tryGrowByReslice(n);
if (i >= 0) {
return i;
}
const c = this.capacity;
if (n <= Math.floor(c / 2) - m) {
// We can slide things down instead of allocating a new
// ArrayBuffer. We only need m+n <= c to slide, but
// we instead let capacity get twice as large so we
// don't spend all our time copying.
copy(this._buf.subarray(this._off), this._buf);
} else if (c + n > MAX_SIZE) {
throw new Error("The buffer cannot be grown beyond the maximum size.");
} else {
// Not enough space anywhere, we need to allocate.
const buf = new Uint8Array(Math.min(2 * c + n, MAX_SIZE));
copy(this._buf.subarray(this._off), buf);
this._buf = buf;
}
// Restore this.off and len(this._buf).
this._off = 0;
this._reslice(Math.min(m + n, MAX_SIZE));
return m;
}
grow(n: number): void {
if (n < 0) {
throw Error("Buffer._grow: negative count");
}
const m = this._grow(n);
this._reslice(m);
}
readFrom(r: Reader): number {
let n = 0;
const tmp = new Uint8Array(MIN_READ);
while (true) {
const shouldGrow = this.capacity - this.length < MIN_READ;
// read into tmp buffer if there's not enough room
// otherwise read directly into the internal buffer
const buf = shouldGrow
? tmp
: new Uint8Array(this._buf.buffer, this.length);
const nread = r.read(buf);
if (nread === null) {
return n;
}
// write will grow if needed
if (shouldGrow) this.write(buf.subarray(0, nread));
else this._reslice(this.length + nread);
n += nread;
}
}
}
export function readAll(r: Reader): Uint8Array {
const buf = new DenoBuffer();
buf.readFrom(r);
return buf.bytes();
}
export function writeAll(w: Writer, arr: Uint8Array): void {
let nwritten = 0;
while (nwritten < arr.length) {
nwritten += w.write(arr.subarray(nwritten));
}
}

@ -0,0 +1,53 @@
/*
* Copyright 2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const Empty = new Uint8Array(0);
export const TE = new TextEncoder();
export const TD = new TextDecoder();
function concat(...bufs: Uint8Array[]): Uint8Array {
let max = 0;
for (let i = 0; i < bufs.length; i++) {
max += bufs[i].length;
}
const out = new Uint8Array(max);
let index = 0;
for (let i = 0; i < bufs.length; i++) {
out.set(bufs[i], index);
index += bufs[i].length;
}
return out;
}
export function encode(...a: string[]): Uint8Array {
const bufs = [];
for (let i = 0; i < a.length; i++) {
bufs.push(TE.encode(a[i]));
}
if (bufs.length === 0) {
return Empty;
}
if (bufs.length === 1) {
return bufs[0];
}
return concat(...bufs);
}
export function decode(a: Uint8Array): string {
if (!a || a.length === 0) {
return "";
}
return TD.decode(a);
}

@ -0,0 +1,14 @@
/*
* Copyright 2018-2021 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

@ -0,0 +1,299 @@
/*
* Copyright 2020-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Heavily inspired by Golang's https://golang.org/src/net/http/header.go
import { TD, TE } from "./encoders.ts";
import { ErrorCode, Match, MsgHdrs, NatsError } from "./core.ts";
// https://www.ietf.org/rfc/rfc822.txt
// 3.1.2. STRUCTURE OF HEADER FIELDS
//
// Once a field has been unfolded, it may be viewed as being com-
// posed of a field-name followed by a colon (":"), followed by a
// field-body, and terminated by a carriage-return/line-feed.
// The field-name must be composed of printable ASCII characters
// (i.e., characters that have values between 33. and 126.,
// decimal, except colon). The field-body may be composed of any
// ASCII characters, except CR or LF. (While CR and/or LF may be
// present in the actual text, they are removed by the action of
// unfolding the field.)
export function canonicalMIMEHeaderKey(k: string): string {
const a = 97;
const A = 65;
const Z = 90;
const z = 122;
const dash = 45;
const colon = 58;
const start = 33;
const end = 126;
const toLower = a - A;
let upper = true;
const buf: number[] = new Array(k.length);
for (let i = 0; i < k.length; i++) {
let c = k.charCodeAt(i);
if (c === colon || c < start || c > end) {
throw new NatsError(
`'${k[i]}' is not a valid character for a header key`,
ErrorCode.BadHeader,
);
}
if (upper && a <= c && c <= z) {
c -= toLower;
} else if (!upper && A <= c && c <= Z) {
c += toLower;
}
buf[i] = c;
upper = c == dash;
}
return String.fromCharCode(...buf);
}
export function headers(code = 0, description = ""): MsgHdrs {
if ((code === 0 && description !== "") || (code > 0 && description === "")) {
throw new Error("setting status requires both code and description");
}
return new MsgHdrsImpl(code, description);
}
const HEADER = "NATS/1.0";
export class MsgHdrsImpl implements MsgHdrs {
_code: number;
headers: Map<string, string[]>;
_description: string;
constructor(code = 0, description = "") {
this._code = code;
this._description = description;
this.headers = new Map();
}
[Symbol.iterator]() {
return this.headers.entries();
}
size(): number {
return this.headers.size;
}
equals(mh: MsgHdrsImpl): boolean {
if (
mh && this.headers.size === mh.headers.size &&
this._code === mh._code
) {
for (const [k, v] of this.headers) {
const a = mh.values(k);
if (v.length !== a.length) {
return false;
}
const vv = [...v].sort();
const aa = [...a].sort();
for (let i = 0; i < vv.length; i++) {
if (vv[i] !== aa[i]) {
return false;
}
}
}
return true;
}
return false;
}
static decode(a: Uint8Array): MsgHdrsImpl {
const mh = new MsgHdrsImpl();
const s = TD.decode(a);
const lines = s.split("\r\n");
const h = lines[0];
if (h !== HEADER) {
// malformed headers could add extra space without adding a code or description
let str = h.replace(HEADER, "").trim();
if (str.length > 0) {
mh._code = parseInt(str, 10);
if (isNaN(mh._code)) {
mh._code = 0;
}
const scode = mh._code.toString();
str = str.replace(scode, "");
mh._description = str.trim();
}
}
if (lines.length >= 1) {
lines.slice(1).map((s) => {
if (s) {
const idx = s.indexOf(":");
if (idx > -1) {
const k = s.slice(0, idx);
const v = s.slice(idx + 1).trim();
mh.append(k, v);
}
}
});
}
return mh;
}
toString(): string {
if (this.headers.size === 0 && this._code === 0) {
return "";
}
let s = HEADER;
if (this._code > 0 && this._description !== "") {
s += ` ${this._code} ${this._description}`;
}
for (const [k, v] of this.headers) {
for (let i = 0; i < v.length; i++) {
s = `${s}\r\n${k}: ${v[i]}`;
}
}
return `${s}\r\n\r\n`;
}
encode(): Uint8Array {
return TE.encode(this.toString());
}
static validHeaderValue(k: string): string {
const inv = /[\r\n]/;
if (inv.test(k)) {
throw new NatsError(
"invalid header value - \\r and \\n are not allowed.",
ErrorCode.BadHeader,
);
}
return k.trim();
}
keys(): string[] {
const keys = [];
for (const sk of this.headers.keys()) {
keys.push(sk);
}
return keys;
}
findKeys(k: string, match = Match.Exact): string[] {
const keys = this.keys();
switch (match) {
case Match.Exact:
return keys.filter((v) => {
return v === k;
});
case Match.CanonicalMIME:
k = canonicalMIMEHeaderKey(k);
return keys.filter((v) => {
return v === k;
});
default: {
const lci = k.toLowerCase();
return keys.filter((v) => {
return lci === v.toLowerCase();
});
}
}
}
get(k: string, match = Match.Exact): string {
const keys = this.findKeys(k, match);
if (keys.length) {
const v = this.headers.get(keys[0]);
if (v) {
return Array.isArray(v) ? v[0] : v;
}
}
return "";
}
has(k: string, match = Match.Exact): boolean {
return this.findKeys(k, match).length > 0;
}
set(k: string, v: string, match = Match.Exact): void {
this.delete(k, match);
this.append(k, v, match);
}
append(k: string, v: string, match = Match.Exact): void {
// validate the key
const ck = canonicalMIMEHeaderKey(k);
if (match === Match.CanonicalMIME) {
k = ck;
}
// if we get non-sensical ignores/etc, we should try
// to do the right thing and use the first key that matches
const keys = this.findKeys(k, match);
k = keys.length > 0 ? keys[0] : k;
const value = MsgHdrsImpl.validHeaderValue(v);
let a = this.headers.get(k);
if (!a) {
a = [];
this.headers.set(k, a);
}
a.push(value);
}
values(k: string, match = Match.Exact): string[] {
const buf: string[] = [];
const keys = this.findKeys(k, match);
keys.forEach((v) => {
const values = this.headers.get(v);
if (values) {
buf.push(...values);
}
});
return buf;
}
delete(k: string, match = Match.Exact): void {
const keys = this.findKeys(k, match);
keys.forEach((v) => {
this.headers.delete(v);
});
}
get hasError() {
return this._code >= 300;
}
get status(): string {
return `${this._code} ${this._description}`.trim();
}
toRecord(): Record<string, string[]> {
const data = {} as Record<string, string[]>;
this.keys().forEach((v) => {
data[v] = this.values(v);
});
return data;
}
get code(): number {
return this._code;
}
get description(): string {
return this._description;
}
static fromRecord(r: Record<string, string[]>): MsgHdrs {
const h = new MsgHdrsImpl();
for (const k in r) {
h.headers.set(k, r[k]);
}
return h;
}
}

@ -0,0 +1,92 @@
/*
* Copyright 2020-2021 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Deferred, deferred } from "./util.ts";
import { DebugEvents, Status } from "./core.ts";
export interface PH {
flush(p?: Deferred<void>): Promise<void>;
disconnect(): void;
dispatchStatus(status: Status): void;
}
export class Heartbeat {
ph: PH;
interval: number;
maxOut: number;
timer?: number;
pendings: Promise<void>[];
constructor(ph: PH, interval: number, maxOut: number) {
this.ph = ph;
this.interval = interval;
this.maxOut = maxOut;
this.pendings = [];
}
// api to start the heartbeats, since this can be
// spuriously called from dial, ensure we don't
// leak timers
start() {
this.cancel();
this._schedule();
}
// api for canceling the heartbeats, if stale is
// true it will initiate a client disconnect
cancel(stale?: boolean) {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
this._reset();
if (stale) {
this.ph.disconnect();
}
}
_schedule() {
// @ts-ignore: node is not a number - we treat this opaquely
this.timer = setTimeout(() => {
this.ph.dispatchStatus(
{ type: DebugEvents.PingTimer, data: `${this.pendings.length + 1}` },
);
if (this.pendings.length === this.maxOut) {
this.cancel(true);
return;
}
const ping = deferred<void>();
this.ph.flush(ping)
.then(() => {
this._reset();
})
.catch(() => {
// we disconnected - pongs were rejected
this.cancel();
});
this.pendings.push(ping);
this._schedule();
}, this.interval);
}
_reset() {
// clear pendings after resolving them
this.pendings = this.pendings.filter((p) => {
const d = p as Deferred<void>;
d.resolve();
return false;
});
}
}

@ -0,0 +1,139 @@
/*
* Copyright 2022 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Called with the number of missed heartbeats.
* If the function returns true, the monitor will cancel monitoring.
*/
export type IdleHeartbeatFn = (n: number) => boolean;
/**
* IdleHeartbeatOptions
*/
export type IdleHeartbeatOptions = {
/**
* @field maxOut - optional maximum number of missed heartbeats before notifying (default is 2)
*/
maxOut: number;
/**
* @field cancelAfter - optional timer to auto cancel monitoring in millis
*/
cancelAfter: number;
};
export class IdleHeartbeat {
interval: number;
maxOut: number;
cancelAfter: number;
timer?: number;
autoCancelTimer?: number;
last!: number;
missed: number;
count: number;
callback: IdleHeartbeatFn;
/**
* Constructor
* @param interval in millis to check
* @param cb a callback to report when heartbeats are missed
* @param opts monitor options @see IdleHeartbeatOptions
*/
constructor(
interval: number,
cb: IdleHeartbeatFn,
opts: Partial<IdleHeartbeatOptions> = { maxOut: 2 },
) {
this.interval = interval;
this.maxOut = opts?.maxOut || 2;
this.cancelAfter = opts?.cancelAfter || 0;
this.last = Date.now();
this.missed = 0;
this.count = 0;
this.callback = cb;
this._schedule();
}
/**
* cancel monitoring
*/
cancel() {
if (this.autoCancelTimer) {
clearTimeout(this.autoCancelTimer);
}
if (this.timer) {
clearInterval(this.timer);
}
this.timer = 0;
this.autoCancelTimer = 0;
}
/**
* work signals that there was work performed
*/
work() {
this.last = Date.now();
this.missed = 0;
}
/**
* internal api to change the interval, cancelAfter and maxOut
* @param interval
* @param cancelAfter
* @param maxOut
*/
_change(interval: number, cancelAfter = 0, maxOut = 2) {
this.interval = interval;
this.maxOut = maxOut;
this.cancelAfter = cancelAfter;
this.restart();
}
/**
* cancels and restarts the monitoring
*/
restart() {
this.cancel();
this._schedule();
}
/**
* internal api called to start monitoring
*/
_schedule() {
if (this.cancelAfter > 0) {
// @ts-ignore: in node is not a number - we treat this opaquely
this.autoCancelTimer = setTimeout(() => {
this.cancel();
}, this.cancelAfter);
}
// @ts-ignore: in node is not a number - we treat this opaquely
this.timer = setInterval(() => {
this.count++;
if ((Date.now() - this.last) > this.interval) {
this.missed++;
}
if (this.missed >= this.maxOut) {
try {
if (this.callback(this.missed) === true) {
this.cancel();
}
} catch (err) {
console.log(err);
}
}
}, this.interval);
}
}

@ -0,0 +1,129 @@
export { NatsConnectionImpl } from "./nats";
export { Nuid, nuid } from "./nuid";
export type { ServiceClient, TypedSubscriptionOptions } from "./types";
export { MsgImpl } from "./msg";
export { setTransportFactory } from "./transport";
export type { Transport, TransportFactory } from "./transport";
export { Connect, INFO, ProtocolHandler } from "./protocol";
export type { Deferred, Perf, Timeout } from "./util";
export { collect, deferred, delay, extend, render, timeout } from "./util";
export { canonicalMIMEHeaderKey, headers, MsgHdrsImpl } from "./headers";
export { Heartbeat } from "./heartbeats";
export type { PH } from "./heartbeats";
export { MuxSubscription } from "./muxsubscription";
export { DataBuffer } from "./databuffer";
export {
buildAuthenticator,
checkOptions,
checkUnsupportedOption,
} from "./options";
export { RequestOne } from "./request";
export {
credsAuthenticator,
jwtAuthenticator,
nkeyAuthenticator,
tokenAuthenticator,
usernamePasswordAuthenticator,
} from "./authenticator";
export type { Codec } from "./codec";
export { JSONCodec, StringCodec } from "./codec";
export * from "./nkeys";
export type {
DispatchedFn,
IngestionFilterFn,
IngestionFilterFnResult,
ProtocolFilterFn,
} from "./queued_iterator";
export { QueuedIteratorImpl } from "./queued_iterator";
export type { ParserEvent } from "./parser";
export { Kind, Parser, State } from "./parser";
export { DenoBuffer, MAX_SIZE, readAll, writeAll } from "./denobuffer";
export { Bench, Metric } from "./bench";
export type { BenchOpts } from "./bench";
export { TD, TE } from "./encoders";
export { isIP, parseIP } from "./ipparser";
export { TypedSubscription } from "./typedsub";
export type { MsgAdapter, TypedCallback } from "./typedsub";
export {
Base64KeyCodec,
Bucket,
defaultBucketOpts,
NoopKvCodecs,
} from "../jetstream/kv";
export type { SemVer } from "./semver";
export { compare, parseSemVer } from "./semver";
export { Empty } from "./types";
export { extractProtocolMessage } from "./transport";
export type {
ApiError,
Auth,
Authenticator,
ConnectionOptions,
Dispatcher,
Endpoint,
EndpointInfo,
EndpointOptions,
EndpointStats,
JwtAuth,
Msg,
MsgHdrs,
NamedEndpointStats,
Nanos,
NatsConnection,
NKeyAuth,
NoAuth,
Payload,
PublishOptions,
QueuedIterator,
Request,
RequestManyOptions,
RequestOptions,
ReviverFn,
Server,
ServerInfo,
ServersChanged,
Service,
ServiceConfig,
ServiceGroup,
ServiceHandler,
ServiceIdentity,
ServiceInfo,
ServiceMetadata,
ServiceMsg,
ServiceResponse,
ServicesAPI,
ServiceStats,
Stats,
Status,
Sub,
SubOpts,
Subscription,
SubscriptionOptions,
SyncIterator,
TlsOptions,
TokenAuth,
UserPass,
} from "./core";
export {
createInbox,
DebugEvents,
ErrorCode,
Events,
isNatsError,
Match,
NatsError,
RequestStrategy,
ServiceError,
ServiceErrorCodeHeader,
ServiceErrorHeader,
ServiceResponseType,
ServiceVerb,
syncIterator,
} from "./core";
export { SubscriptionImpl, Subscriptions } from "./protocol";

@ -0,0 +1,215 @@
/*
* Copyright 2020-2021 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// JavaScript port of go net/ip/ParseIP
// https://github.com/golang/go/blob/master/src/net/ip.go
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
const IPv4LEN = 4;
const IPv6LEN = 16;
const ASCII0 = 48;
const ASCII9 = 57;
const ASCIIA = 65;
const ASCIIF = 70;
const ASCIIa = 97;
const ASCIIf = 102;
const big = 0xFFFFFF;
export function ipV4(a: number, b: number, c: number, d: number): Uint8Array {
const ip = new Uint8Array(IPv6LEN);
const prefix = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff];
prefix.forEach((v, idx) => {
ip[idx] = v;
});
ip[12] = a;
ip[13] = b;
ip[14] = c;
ip[15] = d;
return ip;
}
export function isIP(h: string) {
return parseIP(h) !== undefined;
}
export function parseIP(h: string): Uint8Array | undefined {
for (let i = 0; i < h.length; i++) {
switch (h[i]) {
case ".":
return parseIPv4(h);
case ":":
return parseIPv6(h);
}
}
return;
}
function parseIPv4(s: string): Uint8Array | undefined {
const ip = new Uint8Array(IPv4LEN);
for (let i = 0; i < IPv4LEN; i++) {
if (s.length === 0) {
return undefined;
}
if (i > 0) {
if (s[0] !== ".") {
return undefined;
}
s = s.substring(1);
}
const { n, c, ok } = dtoi(s);
if (!ok || n > 0xFF) {
return undefined;
}
s = s.substring(c);
ip[i] = n;
}
return ipV4(ip[0], ip[1], ip[2], ip[3]);
}
function parseIPv6(s: string): Uint8Array | undefined {
const ip = new Uint8Array(IPv6LEN);
let ellipsis = -1;
if (s.length >= 2 && s[0] === ":" && s[1] === ":") {
ellipsis = 0;
s = s.substring(2);
if (s.length === 0) {
return ip;
}
}
let i = 0;
while (i < IPv6LEN) {
const { n, c, ok } = xtoi(s);
if (!ok || n > 0xFFFF) {
return undefined;
}
if (c < s.length && s[c] === ".") {
if (ellipsis < 0 && i != IPv6LEN - IPv4LEN) {
return undefined;
}
if (i + IPv4LEN > IPv6LEN) {
return undefined;
}
const ip4 = parseIPv4(s);
if (ip4 === undefined) {
return undefined;
}
ip[i] = ip4[12];
ip[i + 1] = ip4[13];
ip[i + 2] = ip4[14];
ip[i + 3] = ip4[15];
s = "";
i += IPv4LEN;
break;
}
ip[i] = n >> 8;
ip[i + 1] = n;
i += 2;
s = s.substring(c);
if (s.length === 0) {
break;
}
if (s[0] !== ":" || s.length == 1) {
return undefined;
}
s = s.substring(1);
if (s[0] === ":") {
if (ellipsis >= 0) {
return undefined;
}
ellipsis = i;
s = s.substring(1);
if (s.length === 0) {
break;
}
}
}
if (s.length !== 0) {
return undefined;
}
if (i < IPv6LEN) {
if (ellipsis < 0) {
return undefined;
}
const n = IPv6LEN - i;
for (let j = i - 1; j >= ellipsis; j--) {
ip[j + n] = ip[j];
}
for (let j = ellipsis + n - 1; j >= ellipsis; j--) {
ip[j] = 0;
}
} else if (ellipsis >= 0) {
return undefined;
}
return ip;
}
function dtoi(s: string): { n: number; c: number; ok: boolean } {
let i = 0;
let n = 0;
for (
i = 0;
i < s.length && ASCII0 <= s.charCodeAt(i) && s.charCodeAt(i) <= ASCII9;
i++
) {
n = n * 10 + (s.charCodeAt(i) - ASCII0);
if (n >= big) {
return { n: big, c: i, ok: false };
}
}
if (i === 0) {
return { n: 0, c: 0, ok: false };
}
return { n: n, c: i, ok: true };
}
function xtoi(s: string): { n: number; c: number; ok: boolean } {
let n = 0;
let i = 0;
for (i = 0; i < s.length; i++) {
if (ASCII0 <= s.charCodeAt(i) && s.charCodeAt(i) <= ASCII9) {
n *= 16;
n += s.charCodeAt(i) - ASCII0;
} else if (ASCIIa <= s.charCodeAt(i) && s.charCodeAt(i) <= ASCIIf) {
n *= 16;
n += (s.charCodeAt(i) - ASCIIa) + 10;
} else if (ASCIIA <= s.charCodeAt(i) && s.charCodeAt(i) <= ASCIIF) {
n *= 16;
n += (s.charCodeAt(i) - ASCIIA) + 10;
} else {
break;
}
if (n >= big) {
return { n: 0, c: i, ok: false };
}
}
if (i === 0) {
return { n: 0, c: i, ok: false };
}
return { n: n, c: i, ok: true };
}

@ -0,0 +1,102 @@
/*
* mod.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-16 20:16
* Distributed under terms of the MIT license.
*/
export {
Bench,
buildAuthenticator,
canonicalMIMEHeaderKey,
createInbox,
credsAuthenticator,
DebugEvents,
deferred,
Empty,
ErrorCode,
Events,
headers,
JSONCodec,
jwtAuthenticator,
Match,
Metric,
MsgHdrsImpl,
NatsError,
nkeyAuthenticator,
nkeys,
Nuid,
nuid,
RequestStrategy,
ServiceError,
ServiceErrorCodeHeader,
ServiceErrorHeader,
ServiceResponseType,
ServiceVerb,
StringCodec,
syncIterator,
tokenAuthenticator,
usernamePasswordAuthenticator,
} from "./internal_mod";
export type {
ApiError,
Auth,
Authenticator,
BenchOpts,
Codec,
ConnectionOptions,
Deferred,
DispatchedFn,
Endpoint,
EndpointInfo,
EndpointOptions,
EndpointStats,
IngestionFilterFn,
IngestionFilterFnResult,
JwtAuth,
Msg,
MsgAdapter,
MsgHdrs,
NamedEndpointStats,
Nanos,
NatsConnection,
NKeyAuth,
NoAuth,
Payload,
Perf,
ProtocolFilterFn,
PublishOptions,
QueuedIterator,
RequestManyOptions,
RequestOptions,
ReviverFn,
ServerInfo,
ServersChanged,
Service,
ServiceClient,
ServiceConfig,
ServiceGroup,
ServiceHandler,
ServiceIdentity,
ServiceInfo,
ServiceMetadata,
ServiceMsg,
ServiceResponse,
ServicesAPI,
ServiceStats,
Stats,
Status,
Sub,
SubOpts,
Subscription,
SubscriptionOptions,
SyncIterator,
TlsOptions,
TokenAuth,
TypedCallback,
TypedSubscriptionOptions,
UserPass,
} from "./internal_mod";

@ -0,0 +1,116 @@
/*
* Copyright 2020-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { MsgHdrsImpl } from "./headers.ts";
import type { MsgArg } from "./parser.ts";
import { Empty, TD } from "./encoders.ts";
import { Codec, JSONCodec } from "./codec.ts";
import {
ErrorCode,
Msg,
MsgHdrs,
NatsError,
Publisher,
ReviverFn,
} from "./core.ts";
export function isRequestError(msg: Msg): NatsError | null {
// NATS core only considers errors 503s on messages that have no payload
// everything else simply forwarded as part of the message and is considered
// application level information
if (msg && msg.data.length === 0 && msg.headers?.code === 503) {
return NatsError.errorForCode(ErrorCode.NoResponders);
}
return null;
}
export class MsgImpl implements Msg {
_headers?: MsgHdrs;
_msg: MsgArg;
_rdata: Uint8Array;
_reply!: string;
_subject!: string;
publisher: Publisher;
static jc: Codec<unknown>;
constructor(msg: MsgArg, data: Uint8Array, publisher: Publisher) {
this._msg = msg;
this._rdata = data;
this.publisher = publisher;
}
get subject(): string {
if (this._subject) {
return this._subject;
}
this._subject = TD.decode(this._msg.subject);
return this._subject;
}
get reply(): string {
if (this._reply) {
return this._reply;
}
this._reply = TD.decode(this._msg.reply);
return this._reply;
}
get sid(): number {
return this._msg.sid;
}
get headers(): MsgHdrs | undefined {
if (this._msg.hdr > -1 && !this._headers) {
const buf = this._rdata.subarray(0, this._msg.hdr);
this._headers = MsgHdrsImpl.decode(buf);
}
return this._headers;
}
get data(): Uint8Array {
if (!this._rdata) {
return new Uint8Array(0);
}
return this._msg.hdr > -1
? this._rdata.subarray(this._msg.hdr)
: this._rdata;
}
// eslint-ignore-next-line @typescript-eslint/no-explicit-any
respond(
data: Uint8Array = Empty,
opts?: { headers?: MsgHdrs; reply?: string },
): boolean {
if (this.reply) {
this.publisher.publish(this.reply, data, opts);
return true;
}
return false;
}
size(): number {
const subj = this._msg.subject.length;
const reply = this._msg.reply?.length || 0;
const payloadAndHeaders = this._msg.size === -1 ? 0 : this._msg.size;
return subj + reply + payloadAndHeaders;
}
json<T = unknown>(reviver?: ReviverFn): T {
return JSONCodec<T>(reviver).decode(this.data);
}
string(): string {
return TD.decode(this.data);
}
}

@ -0,0 +1,106 @@
/*
* Copyright 2020-2021 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isRequestError } from "./msg.ts";
import { createInbox, ErrorCode, Msg, NatsError, Request } from "./core.ts";
export class MuxSubscription {
baseInbox!: string;
reqs: Map<string, Request>;
constructor() {
this.reqs = new Map<string, Request>();
}
size(): number {
return this.reqs.size;
}
init(prefix?: string): string {
this.baseInbox = `${createInbox(prefix)}.`;
return this.baseInbox;
}
add(r: Request) {
if (!isNaN(r.received)) {
r.received = 0;
}
this.reqs.set(r.token, r);
}
get(token: string): Request | undefined {
return this.reqs.get(token);
}
cancel(r: Request): void {
this.reqs.delete(r.token);
}
getToken(m: Msg): string | null {
const s = m.subject || "";
if (s.indexOf(this.baseInbox) === 0) {
return s.substring(this.baseInbox.length);
}
return null;
}
all(): Request[] {
return Array.from(this.reqs.values());
}
handleError(isMuxPermissionError: boolean, err?: NatsError): boolean {
if (err && err.permissionContext) {
if (isMuxPermissionError) {
// one or more requests queued but mux cannot process them
this.all().forEach((r) => {
r.resolver(err, {} as Msg);
});
return true;
}
const ctx = err.permissionContext;
if (ctx.operation === "publish") {
const req = this.all().find((s) => {
return s.requestSubject === ctx.subject;
});
if (req) {
req.resolver(err, {} as Msg);
return true;
}
}
}
return false;
}
dispatcher() {
return (err: NatsError | null, m: Msg) => {
const token = this.getToken(m);
if (token) {
const r = this.get(token);
if (r) {
if (err === null && m.headers) {
err = isRequestError(m);
}
r.resolver(err, m);
}
}
};
}
close() {
const err = NatsError.errorForCode(ErrorCode.Timeout);
this.reqs.forEach((req) => {
req.resolver(err, {} as Msg);
});
}
}

@ -0,0 +1,545 @@
/*
* Copyright 2018-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { deferred } from "./util";
import { ProtocolHandler, SubscriptionImpl } from "./protocol";
import { Empty } from "./encoders";
import { NatsError, ServiceClient } from "./types";
import type { SemVer } from "./semver";
import { Features, parseSemVer } from "./semver";
import { parseOptions } from "./options";
import { QueuedIteratorImpl } from "./queued_iterator";
import {
RequestMany,
RequestManyOptionsInternal,
RequestOne,
} from "./request";
import { isRequestError } from "./msg";
import { JetStreamManagerImpl } from "../jetstream/jsm";
import { JetStreamClientImpl } from "../jetstream/jsclient";
import { ServiceImpl } from "./service";
import { ServiceClientImpl } from "./serviceclient";
import { JetStreamClient, JetStreamManager } from "../jetstream/types";
import {
ConnectionOptions,
createInbox,
ErrorCode,
JetStreamManagerOptions,
JetStreamOptions,
Msg,
NatsConnection,
Payload,
PublishOptions,
QueuedIterator,
RequestManyOptions,
RequestOptions,
RequestStrategy,
ServerInfo,
Service,
ServiceConfig,
ServicesAPI,
Stats,
Status,
Subscription,
SubscriptionOptions,
} from "./core";
export class NatsConnectionImpl implements NatsConnection {
options: ConnectionOptions;
protocol!: ProtocolHandler;
draining: boolean;
listeners: QueuedIterator<Status>[];
_services!: ServicesAPI;
private constructor(opts: ConnectionOptions) {
this.draining = false;
this.options = parseOptions(opts);
this.listeners = [];
}
public static connect(opts: ConnectionOptions = {}): Promise<NatsConnection> {
return new Promise<NatsConnection>((resolve, reject) => {
const nc = new NatsConnectionImpl(opts);
ProtocolHandler.connect(nc.options, nc)
.then((ph: ProtocolHandler) => {
nc.protocol = ph;
(async function() {
for await (const s of ph.status()) {
nc.listeners.forEach((l) => {
l.push(s);
});
}
})();
resolve(nc);
})
.catch((err: Error) => {
reject(err);
});
});
}
closed(): Promise<void | Error> {
return this.protocol.closed;
}
async close() {
await this.protocol.close();
}
_check(subject: string, sub: boolean, pub: boolean) {
if (this.isClosed()) {
throw NatsError.errorForCode(ErrorCode.ConnectionClosed);
}
if (sub && this.isDraining()) {
throw NatsError.errorForCode(ErrorCode.ConnectionDraining);
}
if (pub && this.protocol.noMorePublishing) {
throw NatsError.errorForCode(ErrorCode.ConnectionDraining);
}
subject = subject || "";
if (subject.length === 0) {
throw NatsError.errorForCode(ErrorCode.BadSubject);
}
}
publish(
subject: string,
data?: Payload,
options?: PublishOptions,
): void {
this._check(subject, false, true);
this.protocol.publish(subject, data, options);
}
subscribe(
subject: string,
opts: SubscriptionOptions = {},
): Subscription {
this._check(subject, true, false);
const sub = new SubscriptionImpl(this.protocol, subject, opts);
this.protocol.subscribe(sub);
return sub;
}
_resub(s: Subscription, subject: string, max?: number) {
this._check(subject, true, false);
const si = s as SubscriptionImpl;
// FIXME: need way of understanding a callbacks processed
// count without it, we cannot really do much - ie
// for rejected messages, the count would be lower, etc.
// To handle cases were for example KV is building a map
// the consumer would say how many messages we need to do
// a proper build before we can handle updates.
si.max = max; // this might clear it
if (max) {
// we cannot auto-unsub, because we don't know the
// number of messages we processed vs received
// allow the auto-unsub on processMsg to work if they
// we were called with a new max
si.max = max + si.received;
}
this.protocol.resub(si, subject);
}
// possibilities are:
// stop on error or any non-100 status
// AND:
// - wait for timer
// - wait for n messages or timer
// - wait for unknown messages, done when empty or reset timer expires (with possible alt wait)
// - wait for unknown messages, done when an empty payload is received or timer expires (with possible alt wait)
requestMany(
subject: string,
data: Payload = Empty,
opts: Partial<RequestManyOptions> = { maxWait: 1000, maxMessages: -1 },
): Promise<QueuedIterator<Msg>> {
try {
this._check(subject, true, true);
} catch (err) {
return Promise.reject(err);
}
opts.strategy = opts.strategy || RequestStrategy.Timer;
opts.maxWait = opts.maxWait || 1000;
if (opts.maxWait < 1) {
return Promise.reject(new NatsError("timeout", ErrorCode.InvalidOption));
}
// the iterator for user results
const qi = new QueuedIteratorImpl<Msg>();
function stop(err?: Error) {
//@ts-ignore: stop function
qi.push(() => {
qi.stop(err);
});
}
// callback for the subscription or the mux handler
// simply pushes errors and messages into the iterator
function callback(err: Error | null, msg: Msg | null) {
if (err || msg === null) {
stop(err === null ? undefined : err);
} else {
qi.push(msg);
}
}
if (opts.noMux) {
// we setup a subscription and manage it
const stack = new Error().stack;
let max = typeof opts.maxMessages === "number" && opts.maxMessages > 0
? opts.maxMessages
: -1;
const sub = this.subscribe(createInbox(this.options.inboxPrefix), {
callback: (err, msg) => {
// we only expect runtime errors or a no responders
if (
msg?.data?.length === 0 &&
msg?.headers?.status === ErrorCode.NoResponders
) {
err = NatsError.errorForCode(ErrorCode.NoResponders);
}
// augment any error with the current stack to provide context
// for the error on the suer code
if (err) {
err.stack += `\n\n${stack}`;
cancel(err);
return;
}
// push the message
callback(null, msg);
// see if the m request is completed
if (opts.strategy === RequestStrategy.Count) {
max--;
if (max === 0) {
cancel();
}
}
if (opts.strategy === RequestStrategy.JitterTimer) {
clearTimers();
timer = setTimeout(() => {
cancel();
}, 300);
}
if (opts.strategy === RequestStrategy.SentinelMsg) {
if (msg && msg.data.length === 0) {
cancel();
}
}
},
});
sub.closed
.then(() => {
stop();
})
.catch((err: Error) => {
qi.stop(err);
});
const cancel = (err?: Error) => {
if (err) {
//@ts-ignore: error
qi.push(() => {
throw err;
});
}
clearTimers();
sub.drain()
.then(() => {
stop();
})
.catch((_err: Error) => {
stop();
});
};
qi.iterClosed
.then(() => {
clearTimers();
sub?.unsubscribe();
})
.catch((_err) => {
clearTimers();
sub?.unsubscribe();
});
try {
this.publish(subject, data, { reply: sub.getSubject() });
} catch (err) {
cancel(err);
}
let timer = setTimeout(() => {
cancel();
}, opts.maxWait);
const clearTimers = () => {
if (timer) {
clearTimeout(timer);
}
};
} else {
// the ingestion is the RequestMany
const rmo = opts as RequestManyOptionsInternal;
rmo.callback = callback;
qi.iterClosed.then(() => {
r.cancel();
}).catch((err) => {
r.cancel(err);
});
const r = new RequestMany(this.protocol.muxSubscriptions, subject, rmo);
this.protocol.request(r);
try {
this.publish(
subject,
data,
{
reply: `${this.protocol.muxSubscriptions.baseInbox}${r.token}`,
headers: opts.headers,
},
);
} catch (err) {
r.cancel(err);
}
}
return Promise.resolve(qi);
}
request(
subject: string,
data?: Payload,
opts: RequestOptions = { timeout: 1000, noMux: false },
): Promise<Msg> {
try {
this._check(subject, true, true);
} catch (err) {
return Promise.reject(err);
}
opts.timeout = opts.timeout || 1000;
if (opts.timeout < 1) {
return Promise.reject(new NatsError("timeout", ErrorCode.InvalidOption));
}
if (!opts.noMux && opts.reply) {
return Promise.reject(
new NatsError(
"reply can only be used with noMux",
ErrorCode.InvalidOption,
),
);
}
if (opts.noMux) {
const inbox = opts.reply
? opts.reply
: createInbox(this.options.inboxPrefix);
const d = deferred<Msg>();
const errCtx = new Error();
const sub = this.subscribe(
inbox,
{
max: 1,
timeout: opts.timeout,
callback: (err, msg) => {
if (err) {
// timeouts from `timeout()` will have the proper stack
if (err.code !== ErrorCode.Timeout) {
err.stack += `\n\n${errCtx.stack}`;
}
d.reject(err);
} else {
err = isRequestError(msg);
if (err) {
// if we failed here, help the developer by showing what failed
err.stack += `\n\n${errCtx.stack}`;
d.reject(err);
} else {
d.resolve(msg);
}
}
},
},
);
(sub as SubscriptionImpl).requestSubject = subject;
this.protocol.publish(subject, data, {
reply: inbox,
headers: opts.headers,
});
return d;
} else {
const r = new RequestOne(this.protocol.muxSubscriptions, subject, opts);
this.protocol.request(r);
try {
this.publish(
subject,
data,
{
reply: `${this.protocol.muxSubscriptions.baseInbox}${r.token}`,
headers: opts.headers,
},
);
} catch (err) {
r.cancel(err);
}
const p = Promise.race([r.timer, r.deferred]);
p.catch(() => {
r.cancel();
});
return p;
}
}
/** *
* Flushes to the server. Promise resolves when round-trip completes.
* @returns {Promise<void>}
*/
flush(): Promise<void> {
if (this.isClosed()) {
return Promise.reject(
NatsError.errorForCode(ErrorCode.ConnectionClosed),
);
}
return this.protocol.flush();
}
drain(): Promise<void> {
if (this.isClosed()) {
return Promise.reject(
NatsError.errorForCode(ErrorCode.ConnectionClosed),
);
}
if (this.isDraining()) {
return Promise.reject(
NatsError.errorForCode(ErrorCode.ConnectionDraining),
);
}
this.draining = true;
return this.protocol.drain();
}
isClosed(): boolean {
return this.protocol.isClosed();
}
isDraining(): boolean {
return this.draining;
}
getServer(): string {
const srv = this.protocol.getServer();
return srv ? srv.listen : "";
}
status(): AsyncIterable<Status> {
const iter = new QueuedIteratorImpl<Status>();
iter.iterClosed.then(() => {
const idx = this.listeners.indexOf(iter);
this.listeners.splice(idx, 1);
});
this.listeners.push(iter);
return iter;
}
get info(): ServerInfo | undefined {
return this.protocol.isClosed() ? undefined : this.protocol.info;
}
stats(): Stats {
return {
inBytes: this.protocol.inBytes,
outBytes: this.protocol.outBytes,
inMsgs: this.protocol.inMsgs,
outMsgs: this.protocol.outMsgs,
};
}
async jetstreamManager(
opts: JetStreamManagerOptions = {},
): Promise<JetStreamManager> {
const adm = new JetStreamManagerImpl(this, opts);
if (opts.checkAPI !== false) {
try {
await adm.getAccountInfo();
} catch (err) {
const ne = err as NatsError;
if (ne.code === ErrorCode.NoResponders) {
ne.code = ErrorCode.JetStreamNotEnabled;
}
throw ne;
}
}
return adm;
}
jetstream(
opts: JetStreamOptions = {},
): JetStreamClient {
return new JetStreamClientImpl(this, opts);
}
getServerVersion(): SemVer | undefined {
const info = this.info;
return info ? parseSemVer(info.version) : undefined;
}
async rtt(): Promise<number> {
if (!this.protocol._closed && !this.protocol.connected) {
throw NatsError.errorForCode(ErrorCode.Disconnect);
}
const start = Date.now();
await this.flush();
return Date.now() - start;
}
get features(): Features {
return this.protocol.features;
}
get services(): ServicesAPI {
if (!this._services) {
this._services = new ServicesFactory(this);
}
return this._services;
}
}
export class ServicesFactory implements ServicesAPI {
nc: NatsConnection;
constructor(nc: NatsConnection) {
this.nc = nc;
}
add(config: ServiceConfig): Promise<Service> {
try {
const s = new ServiceImpl(this.nc, config);
return s.start();
} catch (err) {
return Promise.reject(err);
}
}
client(opts?: RequestManyOptions, prefix?: string): ServiceClient {
return new ServiceClientImpl(this.nc, opts, prefix);
}
}

@ -0,0 +1,2 @@
// export * as nkeys from "https://raw.githubusercontent.com/nats-io/nkeys.js/v1.0.4/modules/esm/mod.ts";
export * as nkeys from '../nkey/mod'

@ -0,0 +1,126 @@
/*
* Copyright 2016-2021 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const base = 36;
const preLen = 12;
const seqLen = 10;
const maxSeq = 3656158440062976; // base^seqLen == 36^10
const minInc = 33;
const maxInc = 333;
const totalLen = preLen + seqLen;
function _getRandomValues(a: Uint8Array) {
for (let i = 0; i < a.length; i++) {
a[i] = Math.floor(Math.random() * 255);
}
}
function fillRandom(a: Uint8Array) {
if (globalThis?.crypto?.getRandomValues) {
globalThis.crypto.getRandomValues(a);
} else {
_getRandomValues(a);
}
}
/**
* Create and initialize a nuid.
*
* @api private
*/
export class Nuid {
buf: Uint8Array;
seq!: number;
inc!: number;
constructor() {
this.buf = new Uint8Array(totalLen);
this.init();
}
/**
* Initializes a nuid with a crypto random prefix,
* and pseudo-random sequence and increment.
*
* @api private
*/
private init() {
this.setPre();
this.initSeqAndInc();
this.fillSeq();
}
/**
* Initializes the pseudo randmon sequence number and the increment range.
*
* @api private
*/
private initSeqAndInc() {
this.seq = Math.floor(Math.random() * maxSeq);
this.inc = Math.floor(Math.random() * (maxInc - minInc) + minInc);
}
/**
* Sets the prefix from crypto random bytes. Converts to base36.
*
* @api private
*/
private setPre() {
const cbuf = new Uint8Array(preLen);
fillRandom(cbuf);
for (let i = 0; i < preLen; i++) {
const di = cbuf[i] % base;
this.buf[i] = digits.charCodeAt(di);
}
}
/**
* Fills the sequence part of the nuid as base36 from this.seq.
*
* @api private
*/
private fillSeq() {
let n = this.seq;
for (let i = totalLen - 1; i >= preLen; i--) {
this.buf[i] = digits.charCodeAt(n % base);
n = Math.floor(n / base);
}
}
/**
* Returns the next nuid.
*
* @api private
*/
next(): string {
this.seq += this.inc;
if (this.seq > maxSeq) {
this.setPre();
this.initSeqAndInc();
}
this.fillSeq();
// @ts-ignore - Uint8Arrays can be an argument
return String.fromCharCode.apply(String, this.buf);
}
reset() {
this.init();
}
}
export const nuid = new Nuid();

@ -0,0 +1,166 @@
/*
* Copyright 2021-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { extend } from "./util.ts";
import { defaultPort, getResolveFn } from "./transport.ts";
import { createInbox, ServerInfo } from "./core.ts";
import {
multiAuthenticator,
noAuthFn,
tokenAuthenticator,
usernamePasswordAuthenticator,
} from "./authenticator.ts";
import {
Authenticator,
ConnectionOptions,
DEFAULT_HOST,
ErrorCode,
NatsError,
} from "./core.ts";
export const DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
export const DEFAULT_JITTER = 100;
export const DEFAULT_JITTER_TLS = 1000;
// Ping interval
export const DEFAULT_PING_INTERVAL = 2 * 60 * 1000; // 2 minutes
export const DEFAULT_MAX_PING_OUT = 2;
// DISCONNECT Parameters, 2 sec wait, 10 tries
export const DEFAULT_RECONNECT_TIME_WAIT = 2 * 1000;
export function defaultOptions(): ConnectionOptions {
return {
maxPingOut: DEFAULT_MAX_PING_OUT,
maxReconnectAttempts: DEFAULT_MAX_RECONNECT_ATTEMPTS,
noRandomize: false,
pedantic: false,
pingInterval: DEFAULT_PING_INTERVAL,
reconnect: true,
reconnectJitter: DEFAULT_JITTER,
reconnectJitterTLS: DEFAULT_JITTER_TLS,
reconnectTimeWait: DEFAULT_RECONNECT_TIME_WAIT,
tls: undefined,
verbose: false,
waitOnFirstConnect: false,
ignoreAuthErrorAbort: false,
} as ConnectionOptions;
}
export function buildAuthenticator(
opts: ConnectionOptions,
): Authenticator {
const buf: Authenticator[] = [];
// jwtAuthenticator is created by the user, since it
// will require possibly reading files which
// some of the clients are simply unable to do
if (typeof opts.authenticator === "function") {
buf.push(opts.authenticator);
}
if (Array.isArray(opts.authenticator)) {
buf.push(...opts.authenticator);
}
if (opts.token) {
buf.push(tokenAuthenticator(opts.token));
}
if (opts.user) {
buf.push(usernamePasswordAuthenticator(opts.user, opts.pass));
}
return buf.length === 0 ? noAuthFn() : multiAuthenticator(buf);
}
export function parseOptions(opts?: ConnectionOptions): ConnectionOptions {
const dhp = `${DEFAULT_HOST}:${defaultPort()}`;
opts = opts || { servers: [dhp] };
opts.servers = opts.servers || [];
if (typeof opts.servers === "string") {
opts.servers = [opts.servers];
}
if (opts.servers.length > 0 && opts.port) {
throw new NatsError(
"port and servers options are mutually exclusive",
ErrorCode.InvalidOption,
);
}
if (opts.servers.length === 0 && opts.port) {
opts.servers = [`${DEFAULT_HOST}:${opts.port}`];
}
if (opts.servers && opts.servers.length === 0) {
opts.servers = [dhp];
}
const options = extend(defaultOptions(), opts);
options.authenticator = buildAuthenticator(options);
["reconnectDelayHandler", "authenticator"].forEach((n) => {
if (options[n] && typeof options[n] !== "function") {
throw new NatsError(
`${n} option should be a function`,
ErrorCode.NotFunction,
);
}
});
if (!options.reconnectDelayHandler) {
options.reconnectDelayHandler = () => {
let extra = options.tls
? options.reconnectJitterTLS
: options.reconnectJitter;
if (extra) {
extra++;
extra = Math.floor(Math.random() * extra);
}
return options.reconnectTimeWait + extra;
};
}
if (options.inboxPrefix) {
try {
createInbox(options.inboxPrefix);
} catch (err) {
throw new NatsError(err.message, ErrorCode.ApiError);
}
}
if (options.resolve) {
if (typeof getResolveFn() !== "function") {
throw new NatsError(
`'resolve' is not supported on this client`,
ErrorCode.InvalidOption,
);
}
}
return options;
}
export function checkOptions(info: ServerInfo, options: ConnectionOptions) {
const { proto, tls_required: tlsRequired, tls_available: tlsAvailable } =
info;
if ((proto === undefined || proto < 1) && options.noEcho) {
throw new NatsError("noEcho", ErrorCode.ServerOptionNotAvailable);
}
const tls = tlsRequired || tlsAvailable || false;
if (options.tls && !tls) {
throw new NatsError("tls", ErrorCode.ServerOptionNotAvailable);
}
}
export function checkUnsupportedOption(prop: string, v?: string) {
if (v) {
throw new NatsError(prop, ErrorCode.InvalidOption);
}
}

@ -0,0 +1,748 @@
// deno-lint-ignore-file no-undef
/*
* Copyright 2020-2021 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DenoBuffer } from "./denobuffer.ts";
import { TD } from "./encoders.ts";
import { Dispatcher } from "./core.ts";
export enum Kind {
OK,
ERR,
MSG,
INFO,
PING,
PONG,
}
export interface ParserEvent {
kind: Kind;
msg?: MsgArg;
data?: Uint8Array;
}
export function describe(e: ParserEvent): string {
let ks: string;
let data = "";
switch (e.kind) {
case Kind.MSG:
ks = "MSG";
break;
case Kind.OK:
ks = "OK";
break;
case Kind.ERR:
ks = "ERR";
data = TD.decode(e.data);
break;
case Kind.PING:
ks = "PING";
break;
case Kind.PONG:
ks = "PONG";
break;
case Kind.INFO:
ks = "INFO";
data = TD.decode(e.data);
}
return `${ks}: ${data}`;
}
export interface MsgArg {
subject: Uint8Array;
reply?: Uint8Array;
sid: number;
hdr: number;
size: number;
}
function newMsgArg(): MsgArg {
const ma = {} as MsgArg;
ma.sid = -1;
ma.hdr = -1;
ma.size = -1;
return ma;
}
const ASCII_0 = 48;
const ASCII_9 = 57;
// This is an almost verbatim port of the Go NATS parser
// https://github.com/nats-io/nats.go/blob/master/parser.go
export class Parser {
dispatcher: Dispatcher<ParserEvent>;
state: State;
as: number;
drop: number;
hdr: number;
ma!: MsgArg;
argBuf?: DenoBuffer;
msgBuf?: DenoBuffer;
constructor(dispatcher: Dispatcher<ParserEvent>) {
this.dispatcher = dispatcher;
this.state = State.OP_START;
this.as = 0;
this.drop = 0;
this.hdr = 0;
}
parse(buf: Uint8Array): void {
let i: number;
for (i = 0; i < buf.length; i++) {
const b = buf[i];
switch (this.state) {
case State.OP_START:
switch (b) {
case cc.M:
case cc.m:
this.state = State.OP_M;
this.hdr = -1;
this.ma = newMsgArg();
break;
case cc.H:
case cc.h:
this.state = State.OP_H;
this.hdr = 0;
this.ma = newMsgArg();
break;
case cc.P:
case cc.p:
this.state = State.OP_P;
break;
case cc.PLUS:
this.state = State.OP_PLUS;
break;
case cc.MINUS:
this.state = State.OP_MINUS;
break;
case cc.I:
case cc.i:
this.state = State.OP_I;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_H:
switch (b) {
case cc.M:
case cc.m:
this.state = State.OP_M;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_M:
switch (b) {
case cc.S:
case cc.s:
this.state = State.OP_MS;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_MS:
switch (b) {
case cc.G:
case cc.g:
this.state = State.OP_MSG;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_MSG:
switch (b) {
case cc.SPACE:
case cc.TAB:
this.state = State.OP_MSG_SPC;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_MSG_SPC:
switch (b) {
case cc.SPACE:
case cc.TAB:
continue;
default:
this.state = State.MSG_ARG;
this.as = i;
}
break;
case State.MSG_ARG:
switch (b) {
case cc.CR:
this.drop = 1;
break;
case cc.NL: {
const arg: Uint8Array = this.argBuf
? this.argBuf.bytes()
: buf.subarray(this.as, i - this.drop);
this.processMsgArgs(arg);
this.drop = 0;
this.as = i + 1;
this.state = State.MSG_PAYLOAD;
// jump ahead with the index. If this overruns
// what is left we fall out and process a split buffer.
i = this.as + this.ma.size - 1;
break;
}
default:
if (this.argBuf) {
this.argBuf.writeByte(b);
}
}
break;
case State.MSG_PAYLOAD:
if (this.msgBuf) {
if (this.msgBuf.length >= this.ma.size) {
const data = this.msgBuf.bytes({ copy: false });
this.dispatcher.push(
{ kind: Kind.MSG, msg: this.ma, data: data },
);
this.argBuf = undefined;
this.msgBuf = undefined;
this.state = State.MSG_END;
} else {
let toCopy = this.ma.size - this.msgBuf.length;
const avail = buf.length - i;
if (avail < toCopy) {
toCopy = avail;
}
if (toCopy > 0) {
this.msgBuf.write(buf.subarray(i, i + toCopy));
i = (i + toCopy) - 1;
} else {
this.msgBuf.writeByte(b);
}
}
} else if (i - this.as >= this.ma.size) {
this.dispatcher.push(
{ kind: Kind.MSG, msg: this.ma, data: buf.subarray(this.as, i) },
);
this.argBuf = undefined;
this.msgBuf = undefined;
this.state = State.MSG_END;
}
break;
case State.MSG_END:
switch (b) {
case cc.NL:
this.drop = 0;
this.as = i + 1;
this.state = State.OP_START;
break;
default:
continue;
}
break;
case State.OP_PLUS:
switch (b) {
case cc.O:
case cc.o:
this.state = State.OP_PLUS_O;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_PLUS_O:
switch (b) {
case cc.K:
case cc.k:
this.state = State.OP_PLUS_OK;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_PLUS_OK:
switch (b) {
case cc.NL:
this.dispatcher.push({ kind: Kind.OK });
this.drop = 0;
this.state = State.OP_START;
break;
}
break;
case State.OP_MINUS:
switch (b) {
case cc.E:
case cc.e:
this.state = State.OP_MINUS_E;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_MINUS_E:
switch (b) {
case cc.R:
case cc.r:
this.state = State.OP_MINUS_ER;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_MINUS_ER:
switch (b) {
case cc.R:
case cc.r:
this.state = State.OP_MINUS_ERR;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_MINUS_ERR:
switch (b) {
case cc.SPACE:
case cc.TAB:
this.state = State.OP_MINUS_ERR_SPC;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_MINUS_ERR_SPC:
switch (b) {
case cc.SPACE:
case cc.TAB:
continue;
default:
this.state = State.MINUS_ERR_ARG;
this.as = i;
}
break;
case State.MINUS_ERR_ARG:
switch (b) {
case cc.CR:
this.drop = 1;
break;
case cc.NL: {
let arg: Uint8Array;
if (this.argBuf) {
arg = this.argBuf.bytes();
this.argBuf = undefined;
} else {
arg = buf.subarray(this.as, i - this.drop);
}
this.dispatcher.push({ kind: Kind.ERR, data: arg });
this.drop = 0;
this.as = i + 1;
this.state = State.OP_START;
break;
}
default:
if (this.argBuf) {
this.argBuf.write(Uint8Array.of(b));
}
}
break;
case State.OP_P:
switch (b) {
case cc.I:
case cc.i:
this.state = State.OP_PI;
break;
case cc.O:
case cc.o:
this.state = State.OP_PO;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_PO:
switch (b) {
case cc.N:
case cc.n:
this.state = State.OP_PON;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_PON:
switch (b) {
case cc.G:
case cc.g:
this.state = State.OP_PONG;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_PONG:
switch (b) {
case cc.NL:
this.dispatcher.push({ kind: Kind.PONG });
this.drop = 0;
this.state = State.OP_START;
break;
}
break;
case State.OP_PI:
switch (b) {
case cc.N:
case cc.n:
this.state = State.OP_PIN;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_PIN:
switch (b) {
case cc.G:
case cc.g:
this.state = State.OP_PING;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_PING:
switch (b) {
case cc.NL:
this.dispatcher.push({ kind: Kind.PING });
this.drop = 0;
this.state = State.OP_START;
break;
}
break;
case State.OP_I:
switch (b) {
case cc.N:
case cc.n:
this.state = State.OP_IN;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_IN:
switch (b) {
case cc.F:
case cc.f:
this.state = State.OP_INF;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_INF:
switch (b) {
case cc.O:
case cc.o:
this.state = State.OP_INFO;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_INFO:
switch (b) {
case cc.SPACE:
case cc.TAB:
this.state = State.OP_INFO_SPC;
break;
default:
throw this.fail(buf.subarray(i));
}
break;
case State.OP_INFO_SPC:
switch (b) {
case cc.SPACE:
case cc.TAB:
continue;
default:
this.state = State.INFO_ARG;
this.as = i;
}
break;
case State.INFO_ARG:
switch (b) {
case cc.CR:
this.drop = 1;
break;
case cc.NL: {
let arg: Uint8Array;
if (this.argBuf) {
arg = this.argBuf.bytes();
this.argBuf = undefined;
} else {
arg = buf.subarray(this.as, i - this.drop);
}
this.dispatcher.push({ kind: Kind.INFO, data: arg });
this.drop = 0;
this.as = i + 1;
this.state = State.OP_START;
break;
}
default:
if (this.argBuf) {
this.argBuf.writeByte(b);
}
}
break;
default:
throw this.fail(buf.subarray(i));
}
}
if (
(this.state === State.MSG_ARG || this.state === State.MINUS_ERR_ARG ||
this.state === State.INFO_ARG) && !this.argBuf
) {
this.argBuf = new DenoBuffer(buf.subarray(this.as, i - this.drop));
}
if (this.state === State.MSG_PAYLOAD && !this.msgBuf) {
if (!this.argBuf) {
this.cloneMsgArg();
}
this.msgBuf = new DenoBuffer(buf.subarray(this.as));
}
}
cloneMsgArg() {
const s = this.ma.subject.length;
const r = this.ma.reply ? this.ma.reply.length : 0;
const buf = new Uint8Array(s + r);
buf.set(this.ma.subject);
if (this.ma.reply) {
buf.set(this.ma.reply, s);
}
this.argBuf = new DenoBuffer(buf);
this.ma.subject = buf.subarray(0, s);
if (this.ma.reply) {
this.ma.reply = buf.subarray(s);
}
}
processMsgArgs(arg: Uint8Array): void {
if (this.hdr >= 0) {
return this.processHeaderMsgArgs(arg);
}
const args: Uint8Array[] = [];
let start = -1;
for (let i = 0; i < arg.length; i++) {
const b = arg[i];
switch (b) {
case cc.SPACE:
case cc.TAB:
case cc.CR:
case cc.NL:
if (start >= 0) {
args.push(arg.subarray(start, i));
start = -1;
}
break;
default:
if (start < 0) {
start = i;
}
}
}
if (start >= 0) {
args.push(arg.subarray(start));
}
switch (args.length) {
case 3:
this.ma.subject = args[0];
this.ma.sid = this.protoParseInt(args[1]);
this.ma.reply = undefined;
this.ma.size = this.protoParseInt(args[2]);
break;
case 4:
this.ma.subject = args[0];
this.ma.sid = this.protoParseInt(args[1]);
this.ma.reply = args[2];
this.ma.size = this.protoParseInt(args[3]);
break;
default:
throw this.fail(arg, "processMsgArgs Parse Error");
}
if (this.ma.sid < 0) {
throw this.fail(arg, "processMsgArgs Bad or Missing Sid Error");
}
if (this.ma.size < 0) {
throw this.fail(arg, "processMsgArgs Bad or Missing Size Error");
}
}
fail(data: Uint8Array, label = ""): Error {
if (!label) {
label = `parse error [${this.state}]`;
} else {
label = `${label} [${this.state}]`;
}
return new Error(`${label}: ${TD.decode(data)}`);
}
processHeaderMsgArgs(arg: Uint8Array): void {
const args: Uint8Array[] = [];
let start = -1;
for (let i = 0; i < arg.length; i++) {
const b = arg[i];
switch (b) {
case cc.SPACE:
case cc.TAB:
case cc.CR:
case cc.NL:
if (start >= 0) {
args.push(arg.subarray(start, i));
start = -1;
}
break;
default:
if (start < 0) {
start = i;
}
}
}
if (start >= 0) {
args.push(arg.subarray(start));
}
switch (args.length) {
case 4:
this.ma.subject = args[0];
this.ma.sid = this.protoParseInt(args[1]);
this.ma.reply = undefined;
this.ma.hdr = this.protoParseInt(args[2]);
this.ma.size = this.protoParseInt(args[3]);
break;
case 5:
this.ma.subject = args[0];
this.ma.sid = this.protoParseInt(args[1]);
this.ma.reply = args[2];
this.ma.hdr = this.protoParseInt(args[3]);
this.ma.size = this.protoParseInt(args[4]);
break;
default:
throw this.fail(arg, "processHeaderMsgArgs Parse Error");
}
if (this.ma.sid < 0) {
throw this.fail(arg, "processHeaderMsgArgs Bad or Missing Sid Error");
}
if (this.ma.hdr < 0 || this.ma.hdr > this.ma.size) {
throw this.fail(
arg,
"processHeaderMsgArgs Bad or Missing Header Size Error",
);
}
if (this.ma.size < 0) {
throw this.fail(arg, "processHeaderMsgArgs Bad or Missing Size Error");
}
}
protoParseInt(a: Uint8Array): number {
if (a.length === 0) {
return -1;
}
let n = 0;
for (let i = 0; i < a.length; i++) {
if (a[i] < ASCII_0 || a[i] > ASCII_9) {
return -1;
}
n = n * 10 + (a[i] - ASCII_0);
}
return n;
}
}
export enum State {
OP_START = 0,
OP_PLUS,
OP_PLUS_O,
OP_PLUS_OK,
OP_MINUS,
OP_MINUS_E,
OP_MINUS_ER,
OP_MINUS_ERR,
OP_MINUS_ERR_SPC,
MINUS_ERR_ARG,
OP_M,
OP_MS,
OP_MSG,
OP_MSG_SPC,
MSG_ARG,
MSG_PAYLOAD,
MSG_END,
OP_H,
OP_P,
OP_PI,
OP_PIN,
OP_PING,
OP_PO,
OP_PON,
OP_PONG,
OP_I,
OP_IN,
OP_INF,
OP_INFO,
OP_INFO_SPC,
INFO_ARG,
}
enum cc {
CR = "\r".charCodeAt(0),
E = "E".charCodeAt(0),
e = "e".charCodeAt(0),
F = "F".charCodeAt(0),
f = "f".charCodeAt(0),
G = "G".charCodeAt(0),
g = "g".charCodeAt(0),
H = "H".charCodeAt(0),
h = "h".charCodeAt(0),
I = "I".charCodeAt(0),
i = "i".charCodeAt(0),
K = "K".charCodeAt(0),
k = "k".charCodeAt(0),
M = "M".charCodeAt(0),
m = "m".charCodeAt(0),
MINUS = "-".charCodeAt(0),
N = "N".charCodeAt(0),
n = "n".charCodeAt(0),
NL = "\n".charCodeAt(0),
O = "O".charCodeAt(0),
o = "o".charCodeAt(0),
P = "P".charCodeAt(0),
p = "p".charCodeAt(0),
PLUS = "+".charCodeAt(0),
R = "R".charCodeAt(0),
r = "r".charCodeAt(0),
S = "S".charCodeAt(0),
s = "s".charCodeAt(0),
SPACE = " ".charCodeAt(0),
TAB = "\t".charCodeAt(0),
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,202 @@
/*
* Copyright 2020-2022 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Deferred, deferred } from "./util.ts";
import { ErrorCode, NatsError, QueuedIterator } from "./core.ts";
export type IngestionFilterFnResult = { ingest: boolean; protocol: boolean };
/**
* IngestionFilterFn prevents a value from being ingested by the
* iterator. It is executed on `push`. If ingest is false the value
* shouldn't be pushed. If protcol is true, the value is a protcol
* value
*
* @param: data is the value
* @src: is the source of the data if set.
*/
export type IngestionFilterFn<T = unknown> = (
data: T | null,
src?: unknown,
) => IngestionFilterFnResult;
/**
* ProtocolFilterFn allows filtering of values that shouldn't be presented
* to the iterator. ProtocolFilterFn is executed when a value is about to be presented
*
* @param data: the value
* @returns boolean: true if the value should presented to the iterator
*/
export type ProtocolFilterFn<T = unknown> = (data: T | null) => boolean;
/**
* DispatcherFn allows for values to be processed after being presented
* to the iterator. Note that if the ProtocolFilter rejected the value
* it will _not_ be presented to the DispatchedFn. Any processing should
* instead have been handled by the ProtocolFilterFn.
* @param data: the value
*/
export type DispatchedFn<T = unknown> = (data: T | null) => void;
export class QueuedIteratorImpl<T> implements QueuedIterator<T> {
inflight: number;
processed: number;
// FIXME: this is updated by the protocol
received: number;
noIterator: boolean;
iterClosed: Deferred<void>;
done: boolean;
signal: Deferred<void>;
yields: T[];
filtered: number;
pendingFiltered: number;
ingestionFilterFn?: IngestionFilterFn<T>;
protocolFilterFn?: ProtocolFilterFn<T>;
dispatchedFn?: DispatchedFn<T>;
ctx?: unknown;
_data?: unknown; //data is for use by extenders in any way they like
err?: Error;
time: number;
yielding: boolean;
constructor() {
this.inflight = 0;
this.filtered = 0;
this.pendingFiltered = 0;
this.processed = 0;
this.received = 0;
this.noIterator = false;
this.done = false;
this.signal = deferred<void>();
this.yields = [];
this.iterClosed = deferred<void>();
this.time = 0;
this.yielding = false;
}
[Symbol.asyncIterator]() {
return this.iterate();
}
push(v: T): void {
if (this.done) {
return;
}
if (typeof v === "function") {
this.yields.push(v);
this.signal.resolve();
return;
}
const { ingest, protocol } = this.ingestionFilterFn
? this.ingestionFilterFn(v, this.ctx || this)
: { ingest: true, protocol: false };
if (ingest) {
if (protocol) {
this.filtered++;
this.pendingFiltered++;
}
this.yields.push(v);
this.signal.resolve();
}
}
async *iterate(): AsyncIterableIterator<T> {
if (this.noIterator) {
throw new NatsError("unsupported iterator", ErrorCode.ApiError);
}
if (this.yielding) {
throw new NatsError("already yielding", ErrorCode.ApiError);
}
this.yielding = true;
try {
while (true) {
if (this.yields.length === 0) {
await this.signal;
}
if (this.err) {
throw this.err;
}
const yields = this.yields;
this.inflight = yields.length;
this.yields = [];
for (let i = 0; i < yields.length; i++) {
if (typeof yields[i] === "function") {
const fn = yields[i] as unknown as () => void;
try {
fn();
} catch (err) {
// failed on the invocation - fail the iterator
// so they know to fix the callback
throw err;
}
// fn could have also set an error
if (this.err) {
throw this.err;
}
continue;
}
// only pass messages that pass the filter
const ok = this.protocolFilterFn
? this.protocolFilterFn(yields[i])
: true;
if (ok) {
this.processed++;
const start = Date.now();
yield yields[i];
this.time = Date.now() - start;
if (this.dispatchedFn && yields[i]) {
this.dispatchedFn(yields[i]);
}
} else {
this.pendingFiltered--;
}
this.inflight--;
}
// yielding could have paused and microtask
// could have added messages. Prevent allocations
// if possible
if (this.done) {
break;
} else if (this.yields.length === 0) {
yields.length = 0;
this.yields = yields;
this.signal = deferred();
}
}
} finally {
// the iterator used break/return
this.stop();
}
}
stop(err?: Error): void {
if (this.done) {
return;
}
this.err = err;
this.done = true;
this.signal.resolve();
this.iterClosed.resolve();
}
getProcessed(): number {
return this.noIterator ? this.received : this.processed;
}
getPending(): number {
return this.yields.length + this.inflight - this.pendingFiltered;
}
getReceived(): number {
return this.received - this.filtered;
}
}

@ -0,0 +1,164 @@
/*
* Copyright 2020-2021 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Deferred, deferred, Timeout, timeout } from "./util.ts";
import { MuxSubscription } from "./muxsubscription.ts";
import { nuid } from "./nuid.ts";
import {
ErrorCode,
Msg,
NatsError,
Request,
RequestManyOptions,
RequestOptions,
RequestStrategy,
} from "./core.ts";
export class BaseRequest {
token: string;
received: number;
ctx: Error;
requestSubject: string;
mux: MuxSubscription;
constructor(
mux: MuxSubscription,
requestSubject: string,
) {
this.mux = mux;
this.requestSubject = requestSubject;
this.received = 0;
this.token = nuid.next();
this.ctx = new Error();
}
}
export interface RequestManyOptionsInternal extends RequestManyOptions {
callback: (err: Error | null, msg: Msg | null) => void;
}
/**
* Request expects multiple message response
* the request ends when the timer expires,
* an error arrives or an expected count of messages
* arrives, end is signaled by a null message
*/
export class RequestMany extends BaseRequest implements Request {
callback!: (err: Error | null, msg: Msg | null) => void;
done: Deferred<void>;
timer: number;
max: number;
opts: Partial<RequestManyOptionsInternal>;
constructor(
mux: MuxSubscription,
requestSubject: string,
opts: Partial<RequestManyOptions> = { maxWait: 1000 },
) {
super(mux, requestSubject);
this.opts = opts;
if (typeof this.opts.callback !== "function") {
throw new Error("callback is required");
}
this.callback = this.opts.callback;
this.max = typeof opts.maxMessages === "number" && opts.maxMessages > 0
? opts.maxMessages
: -1;
this.done = deferred();
this.done.then(() => {
this.callback(null, null);
});
// @ts-ignore: node is not a number
this.timer = setTimeout(() => {
this.cancel();
}, opts.maxWait);
}
cancel(err?: NatsError): void {
if (err) {
this.callback(err, null);
}
clearTimeout(this.timer);
this.mux.cancel(this);
this.done.resolve();
}
resolver(err: Error | null, msg: Msg): void {
if (err) {
err.stack += `\n\n${this.ctx.stack}`;
this.cancel(err as NatsError);
} else {
this.callback(null, msg);
if (this.opts.strategy === RequestStrategy.Count) {
this.max--;
if (this.max === 0) {
this.cancel();
}
}
if (this.opts.strategy === RequestStrategy.JitterTimer) {
clearTimeout(this.timer);
// @ts-ignore: node is not a number
this.timer = setTimeout(() => {
this.cancel();
}, this.opts.jitter || 300);
}
if (this.opts.strategy === RequestStrategy.SentinelMsg) {
if (msg && msg.data.length === 0) {
this.cancel();
}
}
}
}
}
export class RequestOne extends BaseRequest implements Request {
deferred: Deferred<Msg>;
timer: Timeout<Msg>;
constructor(
mux: MuxSubscription,
requestSubject: string,
opts: RequestOptions = { timeout: 1000 },
) {
super(mux, requestSubject);
// extend(this, opts);
this.deferred = deferred();
this.timer = timeout<Msg>(opts.timeout);
}
resolver(err: Error | null, msg: Msg): void {
if (this.timer) {
this.timer.cancel();
}
if (err) {
err.stack += `\n\n${this.ctx.stack}`;
this.deferred.reject(err);
} else {
this.deferred.resolve(msg);
}
this.cancel();
}
cancel(err?: NatsError): void {
if (this.timer) {
this.timer.cancel();
}
this.mux.cancel(this);
this.deferred.reject(
err ? err : NatsError.errorForCode(ErrorCode.Cancelled),
);
}
}

@ -0,0 +1,157 @@
/*
* Copyright 2022-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type SemVer = { major: number; minor: number; micro: number };
export function parseSemVer(
s = "",
): SemVer {
const m = s.match(/(\d+).(\d+).(\d+)/);
if (m) {
return {
major: parseInt(m[1]),
minor: parseInt(m[2]),
micro: parseInt(m[3]),
};
}
throw new Error(`'${s}' is not a semver value`);
}
export function compare(a: SemVer, b: SemVer): number {
if (a.major < b.major) return -1;
if (a.major > b.major) return 1;
if (a.minor < b.minor) return -1;
if (a.minor > b.minor) return 1;
if (a.micro < b.micro) return -1;
if (a.micro > b.micro) return 1;
return 0;
}
export enum Feature {
JS_KV = "js_kv",
JS_OBJECTSTORE = "js_objectstore",
JS_PULL_MAX_BYTES = "js_pull_max_bytes",
JS_NEW_CONSUMER_CREATE_API = "js_new_consumer_create",
JS_ALLOW_DIRECT = "js_allow_direct",
JS_MULTIPLE_CONSUMER_FILTER = "js_multiple_consumer_filter",
JS_SIMPLIFICATION = "js_simplification",
JS_STREAM_CONSUMER_METADATA = "js_stream_consumer_metadata",
JS_CONSUMER_FILTER_SUBJECTS = "js_consumer_filter_subjects",
JS_STREAM_FIRST_SEQ = "js_stream_first_seq",
JS_STREAM_SUBJECT_TRANSFORM = "js_stream_subject_transform",
JS_STREAM_SOURCE_SUBJECT_TRANSFORM = "js_stream_source_subject_transform",
JS_STREAM_COMPRESSION = "js_stream_compression",
JS_DEFAULT_CONSUMER_LIMITS = "js_default_consumer_limits",
}
type FeatureVersion = {
ok: boolean;
min: string;
};
export class Features {
server!: SemVer;
features: Map<Feature, FeatureVersion>;
disabled: Feature[];
constructor(v: SemVer) {
this.features = new Map<Feature, FeatureVersion>();
this.disabled = [];
this.update(v);
}
/**
* Removes all disabled entries
*/
resetDisabled() {
this.disabled.length = 0;
this.update(this.server);
}
/**
* Disables a particular feature.
* @param f
*/
disable(f: Feature) {
this.disabled.push(f);
this.update(this.server);
}
isDisabled(f: Feature) {
return this.disabled.indexOf(f) !== -1;
}
update(v: SemVer | string) {
if (typeof v === "string") {
v = parseSemVer(v);
}
this.server = v;
this.set(Feature.JS_KV, "2.6.2");
this.set(Feature.JS_OBJECTSTORE, "2.6.3");
this.set(Feature.JS_PULL_MAX_BYTES, "2.8.3");
this.set(Feature.JS_NEW_CONSUMER_CREATE_API, "2.9.0");
this.set(Feature.JS_ALLOW_DIRECT, "2.9.0");
this.set(Feature.JS_MULTIPLE_CONSUMER_FILTER, "2.10.0");
this.set(Feature.JS_SIMPLIFICATION, "2.9.4");
this.set(Feature.JS_STREAM_CONSUMER_METADATA, "2.10.0");
this.set(Feature.JS_CONSUMER_FILTER_SUBJECTS, "2.10.0");
this.set(Feature.JS_STREAM_FIRST_SEQ, "2.10.0");
this.set(Feature.JS_STREAM_SUBJECT_TRANSFORM, "2.10.0");
this.set(Feature.JS_STREAM_SOURCE_SUBJECT_TRANSFORM, "2.10.0");
this.set(Feature.JS_STREAM_COMPRESSION, "2.10.0");
this.set(Feature.JS_DEFAULT_CONSUMER_LIMITS, "2.10.0");
this.disabled.forEach((f) => {
this.features.delete(f);
});
}
/**
* Register a feature that requires a particular server version.
* @param f
* @param requires
*/
set(f: Feature, requires: string) {
this.features.set(f, {
min: requires,
ok: compare(this.server, parseSemVer(requires)) >= 0,
});
}
/**
* Returns whether the feature is available and the min server
* version that supports it.
* @param f
*/
get(f: Feature): FeatureVersion {
return this.features.get(f) || { min: "unknown", ok: false };
}
/**
* Returns true if the feature is supported
* @param f
*/
supports(f: Feature): boolean {
return this.get(f)?.ok || false;
}
/**
* Returns true if the server is at least the specified version
* @param v
*/
require(v: SemVer | string): boolean {
if (typeof v === "string") {
v = parseSemVer(v);
}
return compare(this.server, v) >= 0;
}
}

@ -0,0 +1,328 @@
/*
* Copyright 2018-2022 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defaultPort, getUrlParseFn } from "./transport.ts";
import { shuffle } from "./util.ts";
import { isIP } from "./ipparser.ts";
import {
DEFAULT_HOST,
DEFAULT_PORT,
DnsResolveFn,
Server,
ServerInfo,
ServersChanged,
} from "./core.ts";
export function isIPV4OrHostname(hp: string): boolean {
if (hp.indexOf(".") !== -1) {
return true;
}
if (hp.indexOf("[") !== -1 || hp.indexOf("::") !== -1) {
return false;
}
// if we have a plain hostname or host:port
if (hp.split(":").length <= 2) {
return true;
}
return false;
}
function isIPV6(hp: string) {
return !isIPV4OrHostname(hp);
}
function filterIpv6MappedToIpv4(hp: string): string {
const prefix = "::FFFF:";
const idx = hp.toUpperCase().indexOf(prefix);
if (idx !== -1 && hp.indexOf(".") !== -1) {
// we have something like: ::FFFF:127.0.0.1 or [::FFFF:127.0.0.1]:4222
let ip = hp.substring(idx + prefix.length);
ip = ip.replace("[", "");
return ip.replace("]", "");
}
return hp;
}
export function hostPort(
u: string,
): { listen: string; hostname: string; port: number } {
u = u.trim();
// remove any protocol that may have been provided
if (u.match(/^(.*:\/\/)(.*)/m)) {
u = u.replace(/^(.*:\/\/)(.*)/gm, "$2");
}
// in web environments, URL may not be a living standard
// that means that protocols other than HTTP/S are not
// parsable correctly.
// the third complication is that we may have been given
// an IPv6 or worse IPv6 mapping an Ipv4
u = filterIpv6MappedToIpv4(u);
// we only wrap cases where they gave us a plain ipv6
// and we are not already bracketed
if (isIPV6(u) && u.indexOf("[") === -1) {
u = `[${u}]`;
}
// if we have ipv6, we expect port after ']:' otherwise after ':'
const op = isIPV6(u) ? u.match(/(]:)(\d+)/) : u.match(/(:)(\d+)/);
const port = op && op.length === 3 && op[1] && op[2]
? parseInt(op[2])
: DEFAULT_PORT;
// the next complication is that new URL() may
// eat ports which match the protocol - so for example
// port 80 may be eliminated - so we flip the protocol
// so that it always yields a value
const protocol = port === 80 ? "https" : "http";
const url = new URL(`${protocol}://${u}`);
url.port = `${port}`;
let hostname = url.hostname;
// if we are bracketed, we need to rip it out
if (hostname.charAt(0) === "[") {
hostname = hostname.substring(1, hostname.length - 1);
}
const listen = url.host;
return { listen, hostname, port };
}
/**
* @hidden
*/
export class ServerImpl implements Server {
src: string;
listen: string;
hostname: string;
port: number;
didConnect: boolean;
reconnects: number;
lastConnect: number;
gossiped: boolean;
tlsName: string;
resolves?: Server[];
constructor(u: string, gossiped = false) {
this.src = u;
this.tlsName = "";
const v = hostPort(u);
this.listen = v.listen;
this.hostname = v.hostname;
this.port = v.port;
this.didConnect = false;
this.reconnects = 0;
this.lastConnect = 0;
this.gossiped = gossiped;
}
toString(): string {
return this.listen;
}
async resolve(
opts: Partial<
{
fn: DnsResolveFn;
randomize: boolean;
resolve: boolean;
debug?: boolean;
}
>,
): Promise<Server[]> {
if (!opts.fn) {
// we cannot resolve - transport doesn't support it
// don't add - to resolves or we get a circ reference
return [this];
}
const buf: Server[] = [];
if (isIP(this.hostname)) {
// don't add - to resolves or we get a circ reference
return [this];
} else {
// resolve the hostname to ips
const ips = await opts.fn(this.hostname);
if (opts.debug) {
console.log(`resolve ${this.hostname} = ${ips.join(",")}`);
}
for (const ip of ips) {
// letting URL handle the details of representing IPV6 ip with a port, etc
// careful to make sure the protocol doesn't line with standard ports or they
// get swallowed
const proto = this.port === 80 ? "https" : "http";
// ipv6 won't be bracketed here, because it came from resolve
const url = new URL(`${proto}://${isIPV6(ip) ? "[" + ip + "]" : ip}`);
url.port = `${this.port}`;
const ss = new ServerImpl(url.host, false);
ss.tlsName = this.hostname;
buf.push(ss);
}
}
if (opts.randomize) {
shuffle(buf);
}
this.resolves = buf;
return buf;
}
}
/**
* @hidden
*/
export class Servers {
private firstSelect: boolean;
private readonly servers: ServerImpl[];
private currentServer: ServerImpl;
private tlsName: string;
private randomize: boolean;
constructor(
listens: string[] = [],
opts: Partial<{ randomize: boolean }> = {},
) {
this.firstSelect = true;
this.servers = [] as ServerImpl[];
this.tlsName = "";
this.randomize = opts.randomize || false;
const urlParseFn = getUrlParseFn();
if (listens) {
listens.forEach((hp) => {
hp = urlParseFn ? urlParseFn(hp) : hp;
this.servers.push(new ServerImpl(hp));
});
if (this.randomize) {
this.servers = shuffle(this.servers);
}
}
if (this.servers.length === 0) {
this.addServer(`${DEFAULT_HOST}:${defaultPort()}`, false);
}
this.currentServer = this.servers[0];
}
clear(): void {
this.servers.length = 0;
}
updateTLSName(): void {
const cs = this.getCurrentServer();
if (!isIP(cs.hostname)) {
this.tlsName = cs.hostname;
this.servers.forEach((s) => {
if (s.gossiped) {
s.tlsName = this.tlsName;
}
});
}
}
getCurrentServer(): ServerImpl {
return this.currentServer;
}
addServer(u: string, implicit = false): void {
const urlParseFn = getUrlParseFn();
u = urlParseFn ? urlParseFn(u) : u;
const s = new ServerImpl(u, implicit);
if (isIP(s.hostname)) {
s.tlsName = this.tlsName;
}
this.servers.push(s);
}
selectServer(): ServerImpl | undefined {
// allow using select without breaking the order of the servers
if (this.firstSelect) {
this.firstSelect = false;
return this.currentServer;
}
const t = this.servers.shift();
if (t) {
this.servers.push(t);
this.currentServer = t;
}
return t;
}
removeCurrentServer(): void {
this.removeServer(this.currentServer);
}
removeServer(server: ServerImpl | undefined): void {
if (server) {
const index = this.servers.indexOf(server);
this.servers.splice(index, 1);
}
}
length(): number {
return this.servers.length;
}
next(): ServerImpl | undefined {
return this.servers.length ? this.servers[0] : undefined;
}
getServers(): ServerImpl[] {
return this.servers;
}
update(info: ServerInfo): ServersChanged {
const added: string[] = [];
let deleted: string[] = [];
const urlParseFn = getUrlParseFn();
const discovered = new Map<string, ServerImpl>();
if (info.connect_urls && info.connect_urls.length > 0) {
info.connect_urls.forEach((hp) => {
hp = urlParseFn ? urlParseFn(hp) : hp;
const s = new ServerImpl(hp, true);
discovered.set(hp, s);
});
}
// remove gossiped servers that are no longer reported
const toDelete: number[] = [];
this.servers.forEach((s, index) => {
const u = s.listen;
if (
s.gossiped && this.currentServer.listen !== u &&
discovered.get(u) === undefined
) {
// server was removed
toDelete.push(index);
}
// remove this entry from reported
discovered.delete(u);
});
// perform the deletion
toDelete.reverse();
toDelete.forEach((index) => {
const removed = this.servers.splice(index, 1);
deleted = deleted.concat(removed[0].listen);
});
// remaining servers are new
discovered.forEach((v, k) => {
this.servers.push(v);
added.push(k);
});
return { added, deleted };
}
}

@ -0,0 +1,676 @@
/*
* Copyright 2022-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Deferred, deferred } from "./util.ts";
import { headers } from "./headers.ts";
import { JSONCodec } from "./codec.ts";
import { nuid } from "./nuid.ts";
import { QueuedIteratorImpl } from "./queued_iterator.ts";
import { nanos, validateName } from "../jetstream/jsutil.ts";
import { parseSemVer } from "./semver.ts";
import { Empty } from "./encoders.ts";
import {
Endpoint,
EndpointInfo,
EndpointOptions,
Msg,
MsgHdrs,
NamedEndpointStats,
Nanos,
NatsConnection,
NatsError,
Payload,
PublishOptions,
QueuedIterator,
ReviverFn,
Service,
ServiceConfig,
ServiceError,
ServiceErrorCodeHeader,
ServiceErrorHeader,
ServiceGroup,
ServiceHandler,
ServiceIdentity,
ServiceInfo,
ServiceMsg,
ServiceResponseType,
ServiceStats,
ServiceVerb,
Sub,
} from "./core.ts";
/**
* Services have common backplane subject pattern:
*
* `$SRV.PING|STATS|INFO` - pings or retrieves status for all services
* `$SRV.PING|STATS|INFO.<name>` - pings or retrieves status for all services having the specified name
* `$SRV.PING|STATS|INFO.<name>.<id>` - pings or retrieves status of a particular service
*
* Note that <name> and <id> are upper-cased.
*/
export const ServiceApiPrefix = "$SRV";
export class ServiceMsgImpl implements ServiceMsg {
msg: Msg;
constructor(msg: Msg) {
this.msg = msg;
}
get data(): Uint8Array {
return this.msg.data;
}
get sid(): number {
return this.msg.sid;
}
get subject(): string {
return this.msg.subject;
}
get reply(): string {
return this.msg.reply || "";
}
get headers(): MsgHdrs | undefined {
return this.msg.headers;
}
respond(data?: Payload, opts?: PublishOptions): boolean {
return this.msg.respond(data, opts);
}
respondError(
code: number,
description: string,
data?: Uint8Array,
opts?: PublishOptions,
): boolean {
opts = opts || {};
opts.headers = opts.headers || headers();
opts.headers?.set(ServiceErrorCodeHeader, `${code}`);
opts.headers?.set(ServiceErrorHeader, description);
return this.msg.respond(data, opts);
}
json<T = unknown>(reviver?: ReviverFn): T {
return this.msg.json(reviver);
}
string(): string {
return this.msg.string();
}
}
export class ServiceGroupImpl implements ServiceGroup {
subject: string;
queue: string;
srv: ServiceImpl;
constructor(parent: ServiceGroup, name = "", queue = "") {
if (name !== "") {
validInternalToken("service group", name);
}
let root = "";
if (parent instanceof ServiceImpl) {
this.srv = parent as ServiceImpl;
root = "";
} else if (parent instanceof ServiceGroupImpl) {
const sg = parent as ServiceGroupImpl;
this.srv = sg.srv;
if (queue === "" && sg.queue !== "") {
queue = sg.queue;
}
root = sg.subject;
} else {
throw new Error("unknown ServiceGroup type");
}
this.subject = this.calcSubject(root, name);
this.queue = queue;
}
calcSubject(root: string, name = ""): string {
if (name === "") {
return root;
}
return root !== "" ? `${root}.${name}` : name;
}
addEndpoint(
name = "",
opts?: ServiceHandler | EndpointOptions,
): QueuedIterator<ServiceMsg> {
opts = opts || { subject: name } as EndpointOptions;
const args: EndpointOptions = typeof opts === "function"
? { handler: opts, subject: name }
: opts;
validateName("endpoint", name);
let { subject, handler, metadata, queue } = args;
subject = subject || name;
queue = queue || this.queue;
validSubjectName("endpoint subject", subject);
subject = this.calcSubject(this.subject, subject);
const ne = { name, subject, queue, handler, metadata };
return this.srv._addEndpoint(ne);
}
addGroup(name = "", queue = ""): ServiceGroup {
return new ServiceGroupImpl(this, name, queue);
}
}
function validSubjectName(context: string, subj: string) {
if (subj === "") {
throw new Error(`${context} cannot be empty`);
}
if (subj.indexOf(" ") !== -1) {
throw new Error(`${context} cannot contain spaces: '${subj}'`);
}
const tokens = subj.split(".");
tokens.forEach((v, idx) => {
if (v === ">" && idx !== tokens.length - 1) {
throw new Error(`${context} cannot have internal '>': '${subj}'`);
}
});
}
function validInternalToken(context: string, subj: string) {
if (subj.indexOf(" ") !== -1) {
throw new Error(`${context} cannot contain spaces: '${subj}'`);
}
const tokens = subj.split(".");
tokens.forEach((v) => {
if (v === ">") {
throw new Error(`${context} name cannot contain internal '>': '${subj}'`);
}
});
}
type NamedEndpoint = {
name: string;
} & Endpoint;
type ServiceSubscription<T = unknown> =
& NamedEndpoint
& {
internal: boolean;
sub: Sub<T>;
qi?: QueuedIterator<T>;
stats: NamedEndpointStatsImpl;
metadata?: Record<string, string>;
};
export class ServiceImpl implements Service {
nc: NatsConnection;
_id: string;
config: ServiceConfig;
handlers: ServiceSubscription[];
internal: ServiceSubscription[];
_stopped: boolean;
_done: Deferred<Error | null>;
started: string;
/**
* @param verb
* @param name
* @param id
* @param prefix - this is only supplied by tooling when building control subject that crosses an account
*/
static controlSubject(
verb: ServiceVerb,
name = "",
id = "",
prefix?: string,
) {
// the prefix is used as is, because it is an
// account boundary permission
const pre = prefix ?? ServiceApiPrefix;
if (name === "" && id === "") {
return `${pre}.${verb}`;
}
validateName("control subject name", name);
if (id !== "") {
validateName("control subject id", id);
return `${pre}.${verb}.${name}.${id}`;
}
return `${pre}.${verb}.${name}`;
}
constructor(
nc: NatsConnection,
config: ServiceConfig = { name: "", version: "" },
) {
this.nc = nc;
this.config = Object.assign({}, config);
if (!this.config.queue) {
this.config.queue = "q";
}
// this will throw if no name
validateName("name", this.config.name);
validateName("queue", this.config.queue);
// this will throw if not semver
parseSemVer(this.config.version);
this._id = nuid.next();
this.internal = [] as ServiceSubscription[];
this._done = deferred();
this._stopped = false;
this.handlers = [];
this.started = new Date().toISOString();
// initialize the stats
this.reset();
// close if the connection closes
this.nc.closed()
.then(() => {
this.close().catch();
})
.catch((err) => {
this.close(err).catch();
});
}
get subjects(): string[] {
return this.handlers.filter((s) => {
return s.internal === false;
}).map((s) => {
return s.subject;
});
}
get id(): string {
return this._id;
}
get name(): string {
return this.config.name;
}
get description(): string {
return this.config.description ?? "";
}
get version(): string {
return this.config.version;
}
get metadata(): Record<string, string> | undefined {
return this.config.metadata;
}
errorToHeader(err: Error): MsgHdrs {
const h = headers();
if (err instanceof ServiceError) {
const se = err as ServiceError;
h.set(ServiceErrorHeader, se.message);
h.set(ServiceErrorCodeHeader, `${se.code}`);
} else {
h.set(ServiceErrorHeader, err.message);
h.set(ServiceErrorCodeHeader, "500");
}
return h;
}
setupHandler(
h: NamedEndpoint,
internal = false,
): ServiceSubscription {
// internals don't use a queue
const queue = internal ? "" : (h.queue ? h.queue : this.config.queue);
const { name, subject, handler } = h as NamedEndpoint;
const sv = h as ServiceSubscription;
sv.internal = internal;
if (internal) {
this.internal.push(sv);
}
sv.stats = new NamedEndpointStatsImpl(name, subject, queue);
sv.queue = queue;
const callback = handler
? (err: NatsError | null, msg: Msg) => {
if (err) {
this.close(err);
return;
}
const start = Date.now();
try {
handler(err, new ServiceMsgImpl(msg));
} catch (err) {
sv.stats.countError(err);
msg?.respond(Empty, { headers: this.errorToHeader(err) });
} finally {
sv.stats.countLatency(start);
}
}
: undefined;
sv.sub = this.nc.subscribe(subject, {
callback,
queue,
});
sv.sub.closed
.then(() => {
if (!this._stopped) {
this.close(new Error(`required subscription ${h.subject} stopped`))
.catch();
}
})
.catch((err) => {
if (!this._stopped) {
const ne = new Error(
`required subscription ${h.subject} errored: ${err.message}`,
);
ne.stack = err.stack;
this.close(ne).catch();
}
});
return sv;
}
info(): ServiceInfo {
return {
type: ServiceResponseType.INFO,
name: this.name,
id: this.id,
version: this.version,
description: this.description,
metadata: this.metadata,
endpoints: this.endpoints(),
} as ServiceInfo;
}
endpoints(): EndpointInfo[] {
return this.handlers.map((v) => {
const { subject, metadata, name, queue } = v;
return { subject, metadata, name, queue_group: queue };
});
}
async stats(): Promise<ServiceStats> {
const endpoints: NamedEndpointStats[] = [];
for (const h of this.handlers) {
if (typeof this.config.statsHandler === "function") {
try {
h.stats.data = await this.config.statsHandler(h);
} catch (err) {
h.stats.countError(err);
}
}
endpoints.push(h.stats.stats(h.qi));
}
return {
type: ServiceResponseType.STATS,
name: this.name,
id: this.id,
version: this.version,
started: this.started,
metadata: this.metadata,
endpoints,
};
}
addInternalHandler(
verb: ServiceVerb,
handler: (err: NatsError | null, msg: Msg) => Promise<void>,
) {
const v = `${verb}`.toUpperCase();
this._doAddInternalHandler(`${v}-all`, verb, handler);
this._doAddInternalHandler(`${v}-kind`, verb, handler, this.name);
this._doAddInternalHandler(
`${v}`,
verb,
handler,
this.name,
this.id,
);
}
_doAddInternalHandler(
name: string,
verb: ServiceVerb,
handler: (err: NatsError | null, msg: Msg) => Promise<void>,
kind = "",
id = "",
) {
const endpoint = {} as NamedEndpoint;
endpoint.name = name;
endpoint.subject = ServiceImpl.controlSubject(verb, kind, id);
endpoint.handler = handler;
this.setupHandler(endpoint, true);
}
start(): Promise<Service> {
const jc = JSONCodec();
const statsHandler = (err: Error | null, msg: Msg): Promise<void> => {
if (err) {
this.close(err);
return Promise.reject(err);
}
return this.stats().then((s) => {
msg?.respond(jc.encode(s));
return Promise.resolve();
});
};
const infoHandler = (err: Error | null, msg: Msg): Promise<void> => {
if (err) {
this.close(err);
return Promise.reject(err);
}
msg?.respond(jc.encode(this.info()));
return Promise.resolve();
};
const ping = jc.encode(this.ping());
const pingHandler = (err: Error | null, msg: Msg): Promise<void> => {
if (err) {
this.close(err).then().catch();
return Promise.reject(err);
}
msg.respond(ping);
return Promise.resolve();
};
this.addInternalHandler(ServiceVerb.PING, pingHandler);
this.addInternalHandler(ServiceVerb.STATS, statsHandler);
this.addInternalHandler(ServiceVerb.INFO, infoHandler);
// now the actual service
this.handlers.forEach((h) => {
const { subject } = h as Endpoint;
if (typeof subject !== "string") {
return;
}
// this is expected in cases where main subject is just
// a root subject for multiple endpoints - user can disable
// listening to the root endpoint, by specifying null
if (h.handler === null) {
return;
}
this.setupHandler(h as unknown as NamedEndpoint);
});
return Promise.resolve(this);
}
close(err?: Error): Promise<null | Error> {
if (this._stopped) {
return this._done;
}
this._stopped = true;
let buf: Promise<void>[] = [];
if (!this.nc.isClosed()) {
buf = this.handlers.concat(this.internal).map((h) => {
return h.sub.drain();
});
}
Promise.allSettled(buf)
.then(() => {
this._done.resolve(err ? err : null);
});
return this._done;
}
get stopped(): Promise<null | Error> {
return this._done;
}
get isStopped(): boolean {
return this._stopped;
}
stop(err?: Error): Promise<null | Error> {
return this.close(err);
}
ping(): ServiceIdentity {
return {
type: ServiceResponseType.PING,
name: this.name,
id: this.id,
version: this.version,
metadata: this.metadata,
};
}
reset(): void {
// pretend we restarted
this.started = new Date().toISOString();
if (this.handlers) {
for (const h of this.handlers) {
h.stats.reset(h.qi);
}
}
}
addGroup(name: string, queue?: string): ServiceGroup {
return new ServiceGroupImpl(this, name, queue);
}
addEndpoint(
name: string,
handler?: ServiceHandler | EndpointOptions,
): QueuedIterator<ServiceMsg> {
const sg = new ServiceGroupImpl(this);
return sg.addEndpoint(name, handler);
}
_addEndpoint(
e: NamedEndpoint,
): QueuedIterator<ServiceMsg> {
const qi = new QueuedIteratorImpl<ServiceMsg>();
qi.noIterator = typeof e.handler === "function";
if (!qi.noIterator) {
e.handler = (err, msg): void => {
err ? this.stop(err).catch() : qi.push(new ServiceMsgImpl(msg));
};
// close the service if the iterator closes
qi.iterClosed.then(() => {
this.close().catch();
});
}
// track the iterator for stats
const ss = this.setupHandler(e, false);
ss.qi = qi;
this.handlers.push(ss);
return qi;
}
}
class NamedEndpointStatsImpl implements NamedEndpointStats {
name: string;
subject: string;
average_processing_time: Nanos;
num_requests: number;
processing_time: Nanos;
num_errors: number;
last_error?: string;
data?: unknown;
metadata?: Record<string, string>;
queue: string;
constructor(name: string, subject: string, queue = "") {
this.name = name;
this.subject = subject;
this.average_processing_time = 0;
this.num_errors = 0;
this.num_requests = 0;
this.processing_time = 0;
this.queue = queue;
}
reset(qi?: QueuedIterator<unknown>) {
this.num_requests = 0;
this.processing_time = 0;
this.average_processing_time = 0;
this.num_errors = 0;
this.last_error = undefined;
this.data = undefined;
const qii = qi as QueuedIteratorImpl<unknown>;
if (qii) {
qii.time = 0;
qii.processed = 0;
}
}
countLatency(start: number) {
this.num_requests++;
this.processing_time += nanos(Date.now() - start);
this.average_processing_time = Math.round(
this.processing_time / this.num_requests,
);
}
countError(err: Error): void {
this.num_errors++;
this.last_error = err.message;
}
_stats(): NamedEndpointStats {
const {
name,
subject,
average_processing_time,
num_errors,
num_requests,
processing_time,
last_error,
data,
queue,
} = this;
return {
name,
subject,
average_processing_time,
num_errors,
num_requests,
processing_time,
last_error,
data,
queue_group: queue,
};
}
stats(qi?: QueuedIterator<unknown>): NamedEndpointStats {
const qii = qi as QueuedIteratorImpl<unknown>;
if (qii?.noIterator === false) {
// grab stats in the iterator
this.processing_time = qii.time;
this.num_requests = qii.processed;
this.average_processing_time =
this.processing_time > 0 && this.num_requests > 0
? this.processing_time / this.num_requests
: 0;
}
return this._stats();
}
}

@ -0,0 +1,98 @@
/*
* Copyright 2022-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Empty } from "./encoders.ts";
import { JSONCodec } from "./codec.ts";
import { QueuedIteratorImpl } from "./queued_iterator.ts";
import {
NatsConnection,
RequestManyOptions,
ServiceIdentity,
ServiceInfo,
ServiceStats,
ServiceVerb,
} from "./core.ts";
import { ServiceImpl } from "./service.ts";
import { QueuedIterator, RequestStrategy, ServiceClient } from "./core.ts";
export class ServiceClientImpl implements ServiceClient {
nc: NatsConnection;
prefix: string | undefined;
opts: RequestManyOptions;
constructor(
nc: NatsConnection,
opts: RequestManyOptions = {
strategy: RequestStrategy.JitterTimer,
maxWait: 2000,
},
prefix?: string,
) {
this.nc = nc;
this.prefix = prefix;
this.opts = opts;
}
ping(
name = "",
id = "",
): Promise<QueuedIterator<ServiceIdentity>> {
return this.q<ServiceIdentity>(ServiceVerb.PING, name, id);
}
stats(
name = "",
id = "",
): Promise<QueuedIterator<ServiceStats>> {
return this.q<ServiceStats>(ServiceVerb.STATS, name, id);
}
info(
name = "",
id = "",
): Promise<QueuedIterator<ServiceInfo>> {
return this.q<ServiceInfo>(ServiceVerb.INFO, name, id);
}
async q<T>(
v: ServiceVerb,
name = "",
id = "",
): Promise<QueuedIterator<T>> {
const iter = new QueuedIteratorImpl<T>();
const jc = JSONCodec<T>();
const subj = ServiceImpl.controlSubject(v, name, id, this.prefix);
const responses = await this.nc.requestMany(subj, Empty, this.opts);
(async () => {
for await (const m of responses) {
try {
const s = jc.decode(m.data);
iter.push(s);
} catch (err) {
// @ts-ignore: pushing fn
iter.push(() => {
iter.stop(err);
});
}
}
//@ts-ignore: push a fn
iter.push(() => {
iter.stop();
});
})().catch((err) => {
iter.stop(err);
});
return iter;
}
}

@ -0,0 +1,360 @@
// deno-fmt-ignore-file
// deno-lint-ignore-file
// This code was bundled using `deno bundle` and it's not recommended to edit it manually
// deno bundle https://deno.land/x/sha256@v1.0.2/mod.ts
// The MIT License (MIT)
//
// Original work (c) Marco Paland (marco@paland.com) 2015-2018, PALANDesign Hannover, Germany
//
// Deno port Copyright (c) 2019 Noah Anabiik Schwarz
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
function getLengths(b64) {
const len = b64.length;
let validLen = b64.indexOf("=");
if (validLen === -1) {
validLen = len;
}
const placeHoldersLen = validLen === len ? 0 : 4 - validLen % 4;
return [
validLen,
placeHoldersLen
];
}
function init(lookup, revLookup, urlsafe = false) {
function _byteLength(validLen, placeHoldersLen) {
return Math.floor((validLen + placeHoldersLen) * 3 / 4 - placeHoldersLen);
}
function tripletToBase64(num) {
return lookup[num >> 18 & 0x3f] + lookup[num >> 12 & 0x3f] + lookup[num >> 6 & 0x3f] + lookup[num & 0x3f];
}
function encodeChunk(buf, start, end) {
const out = new Array((end - start) / 3);
for(let i = start, curTriplet = 0; i < end; i += 3){
out[curTriplet++] = tripletToBase64((buf[i] << 16) + (buf[i + 1] << 8) + buf[i + 2]);
}
return out.join("");
}
return {
byteLength (b64) {
return _byteLength.apply(null, getLengths(b64));
},
toUint8Array (b64) {
const [validLen, placeHoldersLen] = getLengths(b64);
const buf = new Uint8Array(_byteLength(validLen, placeHoldersLen));
const len = placeHoldersLen ? validLen - 4 : validLen;
let tmp;
let curByte = 0;
let i;
for(i = 0; i < len; i += 4){
tmp = revLookup[b64.charCodeAt(i)] << 18 | revLookup[b64.charCodeAt(i + 1)] << 12 | revLookup[b64.charCodeAt(i + 2)] << 6 | revLookup[b64.charCodeAt(i + 3)];
buf[curByte++] = tmp >> 16 & 0xff;
buf[curByte++] = tmp >> 8 & 0xff;
buf[curByte++] = tmp & 0xff;
}
if (placeHoldersLen === 2) {
tmp = revLookup[b64.charCodeAt(i)] << 2 | revLookup[b64.charCodeAt(i + 1)] >> 4;
buf[curByte++] = tmp & 0xff;
} else if (placeHoldersLen === 1) {
tmp = revLookup[b64.charCodeAt(i)] << 10 | revLookup[b64.charCodeAt(i + 1)] << 4 | revLookup[b64.charCodeAt(i + 2)] >> 2;
buf[curByte++] = tmp >> 8 & 0xff;
buf[curByte++] = tmp & 0xff;
}
return buf;
},
fromUint8Array (buf) {
const maxChunkLength = 16383;
const len = buf.length;
const extraBytes = len % 3;
const len2 = len - extraBytes;
const parts = new Array(Math.ceil(len2 / 16383) + (extraBytes ? 1 : 0));
let curChunk = 0;
let chunkEnd;
for(let i = 0; i < len2; i += maxChunkLength){
chunkEnd = i + maxChunkLength;
parts[curChunk++] = encodeChunk(buf, i, chunkEnd > len2 ? len2 : chunkEnd);
}
let tmp;
if (extraBytes === 1) {
tmp = buf[len2];
parts[curChunk] = lookup[tmp >> 2] + lookup[tmp << 4 & 0x3f];
if (!urlsafe) parts[curChunk] += "==";
} else if (extraBytes === 2) {
tmp = buf[len2] << 8 | buf[len2 + 1] & 0xff;
parts[curChunk] = lookup[tmp >> 10] + lookup[tmp >> 4 & 0x3f] + lookup[tmp << 2 & 0x3f];
if (!urlsafe) parts[curChunk] += "=";
}
return parts.join("");
}
};
}
const lookup = [];
const revLookup = [];
const code = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
for(let i = 0, l = code.length; i < l; ++i){
lookup[i] = code[i];
revLookup[code.charCodeAt(i)] = i;
}
const { byteLength , toUint8Array , fromUint8Array } = init(lookup, revLookup, true);
const decoder = new TextDecoder();
const encoder = new TextEncoder();
function toHexString(buf) {
return buf.reduce((hex, __byte)=>`${hex}${__byte < 16 ? "0" : ""}${__byte.toString(16)}`, "");
}
function fromHexString(hex) {
const len = hex.length;
if (len % 2 || !/^[0-9a-fA-F]+$/.test(hex)) {
throw new TypeError("Invalid hex string.");
}
hex = hex.toLowerCase();
const buf = new Uint8Array(Math.floor(len / 2));
const end = len / 2;
for(let i = 0; i < end; ++i){
buf[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return buf;
}
function decode(buf, encoding = "utf8") {
if (/^utf-?8$/i.test(encoding)) {
return decoder.decode(buf);
} else if (/^base64$/i.test(encoding)) {
return fromUint8Array(buf);
} else if (/^hex(?:adecimal)?$/i.test(encoding)) {
return toHexString(buf);
} else {
throw new TypeError("Unsupported string encoding.");
}
}
function encode(str, encoding = "utf8") {
if (/^utf-?8$/i.test(encoding)) {
return encoder.encode(str);
} else if (/^base64$/i.test(encoding)) {
return toUint8Array(str);
} else if (/^hex(?:adecimal)?$/i.test(encoding)) {
return fromHexString(str);
} else {
throw new TypeError("Unsupported string encoding.");
}
}
const BYTES = 32;
class SHA256 {
hashSize = 32;
_buf;
_bufIdx;
_count;
_K;
_H;
_finalized;
constructor(){
this._buf = new Uint8Array(64);
this._K = new Uint32Array([
0x428a2f98,
0x71374491,
0xb5c0fbcf,
0xe9b5dba5,
0x3956c25b,
0x59f111f1,
0x923f82a4,
0xab1c5ed5,
0xd807aa98,
0x12835b01,
0x243185be,
0x550c7dc3,
0x72be5d74,
0x80deb1fe,
0x9bdc06a7,
0xc19bf174,
0xe49b69c1,
0xefbe4786,
0x0fc19dc6,
0x240ca1cc,
0x2de92c6f,
0x4a7484aa,
0x5cb0a9dc,
0x76f988da,
0x983e5152,
0xa831c66d,
0xb00327c8,
0xbf597fc7,
0xc6e00bf3,
0xd5a79147,
0x06ca6351,
0x14292967,
0x27b70a85,
0x2e1b2138,
0x4d2c6dfc,
0x53380d13,
0x650a7354,
0x766a0abb,
0x81c2c92e,
0x92722c85,
0xa2bfe8a1,
0xa81a664b,
0xc24b8b70,
0xc76c51a3,
0xd192e819,
0xd6990624,
0xf40e3585,
0x106aa070,
0x19a4c116,
0x1e376c08,
0x2748774c,
0x34b0bcb5,
0x391c0cb3,
0x4ed8aa4a,
0x5b9cca4f,
0x682e6ff3,
0x748f82ee,
0x78a5636f,
0x84c87814,
0x8cc70208,
0x90befffa,
0xa4506ceb,
0xbef9a3f7,
0xc67178f2
]);
this.init();
}
init() {
this._H = new Uint32Array([
0x6a09e667,
0xbb67ae85,
0x3c6ef372,
0xa54ff53a,
0x510e527f,
0x9b05688c,
0x1f83d9ab,
0x5be0cd19
]);
this._bufIdx = 0;
this._count = new Uint32Array(2);
this._buf.fill(0);
this._finalized = false;
return this;
}
update(msg, inputEncoding) {
if (msg === null) {
throw new TypeError("msg must be a string or Uint8Array.");
} else if (typeof msg === "string") {
msg = encode(msg, inputEncoding);
}
for(let i = 0, len = msg.length; i < len; i++){
this._buf[this._bufIdx++] = msg[i];
if (this._bufIdx === 64) {
this._transform();
this._bufIdx = 0;
}
}
const c = this._count;
if ((c[0] += msg.length << 3) < msg.length << 3) {
c[1]++;
}
c[1] += msg.length >>> 29;
return this;
}
digest(outputEncoding) {
if (this._finalized) {
throw new Error("digest has already been called.");
}
this._finalized = true;
const b = this._buf;
let idx = this._bufIdx;
b[idx++] = 0x80;
while(idx !== 56){
if (idx === 64) {
this._transform();
idx = 0;
}
b[idx++] = 0;
}
const c = this._count;
b[56] = c[1] >>> 24 & 0xff;
b[57] = c[1] >>> 16 & 0xff;
b[58] = c[1] >>> 8 & 0xff;
b[59] = c[1] >>> 0 & 0xff;
b[60] = c[0] >>> 24 & 0xff;
b[61] = c[0] >>> 16 & 0xff;
b[62] = c[0] >>> 8 & 0xff;
b[63] = c[0] >>> 0 & 0xff;
this._transform();
const hash = new Uint8Array(32);
for(let i = 0; i < 8; i++){
hash[(i << 2) + 0] = this._H[i] >>> 24 & 0xff;
hash[(i << 2) + 1] = this._H[i] >>> 16 & 0xff;
hash[(i << 2) + 2] = this._H[i] >>> 8 & 0xff;
hash[(i << 2) + 3] = this._H[i] >>> 0 & 0xff;
}
this.init();
return outputEncoding ? decode(hash, outputEncoding) : hash;
}
_transform() {
const h = this._H;
let h0 = h[0];
let h1 = h[1];
let h2 = h[2];
let h3 = h[3];
let h4 = h[4];
let h5 = h[5];
let h6 = h[6];
let h7 = h[7];
const w = new Uint32Array(16);
let i;
for(i = 0; i < 16; i++){
w[i] = this._buf[(i << 2) + 3] | this._buf[(i << 2) + 2] << 8 | this._buf[(i << 2) + 1] << 16 | this._buf[i << 2] << 24;
}
for(i = 0; i < 64; i++){
let tmp;
if (i < 16) {
tmp = w[i];
} else {
let a = w[i + 1 & 15];
let b = w[i + 14 & 15];
tmp = w[i & 15] = (a >>> 7 ^ a >>> 18 ^ a >>> 3 ^ a << 25 ^ a << 14) + (b >>> 17 ^ b >>> 19 ^ b >>> 10 ^ b << 15 ^ b << 13) + w[i & 15] + w[i + 9 & 15] | 0;
}
tmp = tmp + h7 + (h4 >>> 6 ^ h4 >>> 11 ^ h4 >>> 25 ^ h4 << 26 ^ h4 << 21 ^ h4 << 7) + (h6 ^ h4 & (h5 ^ h6)) + this._K[i] | 0;
h7 = h6;
h6 = h5;
h5 = h4;
h4 = h3 + tmp;
h3 = h2;
h2 = h1;
h1 = h0;
h0 = tmp + (h1 & h2 ^ h3 & (h1 ^ h2)) + (h1 >>> 2 ^ h1 >>> 13 ^ h1 >>> 22 ^ h1 << 30 ^ h1 << 19 ^ h1 << 10) | 0;
}
h[0] = h[0] + h0 | 0;
h[1] = h[1] + h1 | 0;
h[2] = h[2] + h2 | 0;
h[3] = h[3] + h3 | 0;
h[4] = h[4] + h4 | 0;
h[5] = h[5] + h5 | 0;
h[6] = h[6] + h6 | 0;
h[7] = h[7] + h7 | 0;
}
}
function sha256(msg, inputEncoding, outputEncoding) {
return new SHA256().update(msg, inputEncoding).digest(outputEncoding);
}
export { BYTES as BYTES };
export { SHA256 as SHA256 };
export { sha256 as sha256 };

@ -0,0 +1,116 @@
/*
* Copyright 2020-2021 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TD } from "./encoders";
import {
ConnectionOptions,
DEFAULT_PORT,
DnsResolveFn,
Server,
URLParseFn,
} from "./core";
import { DataBuffer } from "./databuffer";
let transportConfig: TransportFactory;
export function setTransportFactory(config: TransportFactory): void {
transportConfig = config;
}
export function defaultPort(): number {
return transportConfig !== undefined &&
transportConfig.defaultPort !== undefined
? transportConfig.defaultPort
: DEFAULT_PORT;
}
export function getUrlParseFn(): URLParseFn | undefined {
return transportConfig !== undefined && transportConfig.urlParseFn
? transportConfig.urlParseFn
: undefined;
}
export function newTransport(): Transport {
if (!transportConfig || typeof transportConfig.factory !== "function") {
throw new Error("transport fn is not set");
}
return transportConfig.factory();
}
export function getResolveFn(): DnsResolveFn | undefined {
return transportConfig !== undefined && transportConfig.dnsResolveFn
? transportConfig.dnsResolveFn
: undefined;
}
export interface TransportFactory {
factory?: () => Transport;
defaultPort?: number;
urlParseFn?: URLParseFn;
dnsResolveFn?: DnsResolveFn;
}
export interface Transport extends AsyncIterable<Uint8Array> {
readonly isClosed: boolean;
readonly lang: string;
readonly version: string;
readonly closeError?: Error;
connect(
server: Server,
opts: ConnectionOptions,
): Promise<void>;
[Symbol.asyncIterator](): AsyncIterableIterator<Uint8Array>;
isEncrypted(): boolean;
send(frame: Uint8Array): void;
close(err?: Error): Promise<void>;
disconnect(): void;
closed(): Promise<void | Error>;
// this is here for websocket implementations as some implementations
// (firefox) throttle connections that then resolve later
discard(): void;
}
export const CR_LF = "\r\n";
export const CR_LF_LEN = CR_LF.length;
export const CRLF = DataBuffer.fromAscii(CR_LF);
export const CR = new Uint8Array(CRLF)[0]; // 13
export const LF = new Uint8Array(CRLF)[1]; // 10
export function protoLen(ba: Uint8Array): number {
for (let i = 0; i < ba.length; i++) {
const n = i + 1;
if (ba.byteLength > n && ba[i] === CR && ba[n] === LF) {
return n + 1;
}
}
return 0;
}
export function extractProtocolMessage(a: Uint8Array): string {
// protocol messages are ascii, so Uint8Array
const len = protoLen(a);
if (len > 0) {
const ba = new Uint8Array(a);
const out = ba.slice(0, len);
return TD.decode(out);
}
return "";
}

@ -0,0 +1,229 @@
/*
* Copyright 2021 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Deferred, deferred } from "./util.ts";
import type {
DispatchedFn,
IngestionFilterFn,
ProtocolFilterFn,
} from "./queued_iterator.ts";
import { QueuedIteratorImpl } from "./queued_iterator.ts";
import {
ErrorCode,
Msg,
NatsConnection,
NatsError,
Sub,
SubOpts,
Subscription,
SubscriptionOptions,
} from "./core.ts";
import { SubscriptionImpl } from "./protocol.ts";
/**
* Converts a NATS message into some other type. Implementers are expected to:
* return [err, null] if the message callback is invoked with an error.
* return [err, null] if converting the message yielded an error, note that
* iterators will stop on the error, but callbacks will be presented with
* the error.
* return [null, T] if the conversion worked correctly
*/
export type MsgAdapter<T> = (
err: NatsError | null,
msg: Msg,
) => [NatsError | null, T | null];
/**
* Callback presented to the user with the converted type
*/
export type TypedCallback<T> = (err: NatsError | null, msg: T | null) => void;
export interface TypedSubscriptionOptions<T> extends SubOpts<T> {
adapter: MsgAdapter<T>;
callback?: TypedCallback<T>;
ingestionFilterFn?: IngestionFilterFn<T>;
protocolFilterFn?: ProtocolFilterFn<T>;
dispatchedFn?: DispatchedFn<T>;
cleanupFn?: (sub: Subscription, info?: unknown) => void;
}
export function checkFn(fn: unknown, name: string, required = false) {
if (required === true && !fn) {
throw NatsError.errorForCode(
ErrorCode.ApiError,
new Error(`${name} is not a function`),
);
}
if (fn && typeof fn !== "function") {
throw NatsError.errorForCode(
ErrorCode.ApiError,
new Error(`${name} is not a function`),
);
}
}
/**
* TypedSubscription wraps a subscription to provide payload specific
* subscription semantics. That is messages are a transport
* for user data, and the data is presented as application specific
* data to the client.
*/
export class TypedSubscription<T> extends QueuedIteratorImpl<T>
implements Sub<T> {
sub: SubscriptionImpl;
adapter: MsgAdapter<T>;
subIterDone: Deferred<void>;
constructor(
nc: NatsConnection,
subject: string,
opts: TypedSubscriptionOptions<T>,
) {
super();
checkFn(opts.adapter, "adapter", true);
this.adapter = opts.adapter;
if (opts.callback) {
checkFn(opts.callback, "callback");
}
this.noIterator = typeof opts.callback === "function";
if (opts.ingestionFilterFn) {
checkFn(opts.ingestionFilterFn, "ingestionFilterFn");
this.ingestionFilterFn = opts.ingestionFilterFn;
}
if (opts.protocolFilterFn) {
checkFn(opts.protocolFilterFn, "protocolFilterFn");
this.protocolFilterFn = opts.protocolFilterFn;
}
if (opts.dispatchedFn) {
checkFn(opts.dispatchedFn, "dispatchedFn");
this.dispatchedFn = opts.dispatchedFn;
}
if (opts.cleanupFn) {
checkFn(opts.cleanupFn, "cleanupFn");
}
let callback = (err: NatsError | null, msg: Msg) => {
this.callback(err, msg);
};
if (opts.callback) {
const uh = opts.callback;
callback = (err: NatsError | null, msg: Msg) => {
const [jer, tm] = this.adapter(err, msg);
if (jer) {
uh(jer, null);
return;
}
const { ingest } = this.ingestionFilterFn
? this.ingestionFilterFn(tm, this)
: { ingest: true };
if (ingest) {
const ok = this.protocolFilterFn ? this.protocolFilterFn(tm) : true;
if (ok) {
uh(jer, tm);
if (this.dispatchedFn && tm) {
this.dispatchedFn(tm);
}
}
}
};
}
const { max, queue, timeout } = opts;
const sopts = { queue, timeout, callback } as SubscriptionOptions;
if (max && max > 0) {
sopts.max = max;
}
this.sub = nc.subscribe(subject, sopts) as SubscriptionImpl;
if (opts.cleanupFn) {
this.sub.cleanupFn = opts.cleanupFn;
}
if (!this.noIterator) {
this.iterClosed.then(() => {
this.unsubscribe();
});
}
this.subIterDone = deferred<void>();
Promise.all([this.sub.closed, this.iterClosed])
.then(() => {
this.subIterDone.resolve();
})
.catch(() => {
this.subIterDone.resolve();
});
(async (s) => {
await s.closed;
this.stop();
})(this.sub).then().catch();
}
unsubscribe(max?: number): void {
this.sub.unsubscribe(max);
}
drain(): Promise<void> {
return this.sub.drain();
}
isDraining(): boolean {
return this.sub.isDraining();
}
isClosed(): boolean {
return this.sub.isClosed();
}
callback(e: NatsError | null, msg: Msg): void {
this.sub.cancelTimeout();
const [err, tm] = this.adapter(e, msg);
if (err) {
this.stop(err);
}
if (tm) {
this.push(tm);
}
}
getSubject(): string {
return this.sub.getSubject();
}
getReceived(): number {
return this.sub.getReceived();
}
getProcessed(): number {
return this.sub.getProcessed();
}
getPending(): number {
return this.sub.getPending();
}
getID(): number {
return this.sub.getID();
}
getMax(): number | undefined {
return this.sub.getMax();
}
get closed(): Promise<void> {
return this.sub.closed;
}
}

@ -0,0 +1,26 @@
/*
* Copyright 2020-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type {
ApiError,
Dispatcher,
MsgHdrs,
QueuedIterator,
ServiceClient,
} from "./core.ts";
export { NatsError } from "./core.ts";
export type { TypedSubscriptionOptions } from "./typedsub.ts";
export { Empty } from "./encoders.ts";

@ -0,0 +1,239 @@
/*
* Copyright 2018-2021 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// deno-lint-ignore-file no-explicit-any
import { TD } from "./encoders";
import { ErrorCode, NatsError } from "./core";
export type ValueResult<T> = {
isError: false;
value: T;
};
export type ErrorResult = {
isError: true;
error: Error;
};
/**
* Result is a value that may have resulted in an error.
*/
export type Result<T> = ValueResult<T> | ErrorResult;
export function extend(a: any, ...b: any[]): any {
for (let i = 0; i < b.length; i++) {
const o = b[i];
Object.keys(o).forEach(function(k) {
a[k] = o[k];
});
}
return a;
}
export interface Pending {
pending: number;
write: (c: number) => void;
wrote: (c: number) => void;
err: (err: Error) => void;
close: () => void;
promise: () => Promise<any>;
resolved: boolean;
done: boolean;
}
export function render(frame: Uint8Array): string {
const cr = "␍";
const lf = "␊";
return TD.decode(frame)
.replace(/\n/g, lf)
.replace(/\r/g, cr);
}
export interface Timeout<T> extends Promise<T> {
cancel: () => void;
}
export function timeout<T>(ms: number): Timeout<T> {
// by generating the stack here to help identify what timed out
const err = NatsError.errorForCode(ErrorCode.Timeout);
let methods;
let timer: number;
const p = new Promise((_resolve, reject) => {
const cancel = (): void => {
if (timer) {
clearTimeout(timer);
}
};
methods = { cancel };
// @ts-ignore: node is not a number
timer = setTimeout(() => {
reject(err);
}, ms);
});
// noinspection JSUnusedAssignment
return Object.assign(p, methods) as Timeout<T>;
}
export function delay(ms = 0): Promise<void> {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
export function deadline<T>(p: Promise<T>, millis = 1000): Promise<T> {
const err = new Error(`deadline exceeded`);
const d = deferred<never>();
const timer = setTimeout(
() => d.reject(err),
millis,
);
return Promise.race([p, d]).finally(() => clearTimeout(timer));
}
export interface Deferred<T> extends Promise<T> {
/**
* Resolves the Deferred to a value T
* @param value
*/
resolve: (value?: T | PromiseLike<T>) => void;
//@ts-ignore: tsc guard
/**
* Rejects the Deferred
* @param reason
*/
reject: (reason?: any) => void;
}
/**
* Returns a Promise that has a resolve/reject methods that can
* be used to resolve and defer the Deferred.
*/
export function deferred<T>(): Deferred<T> {
let methods = {};
const p = new Promise<T>((resolve, reject): void => {
methods = { resolve, reject };
});
return Object.assign(p, methods) as Deferred<T>;
}
export function debugDeferred<T>(): Deferred<T> {
let methods = {};
const p = new Promise<T>((resolve, reject): void => {
methods = {
resolve: (v: T) => {
console.trace("resolve", v);
resolve(v);
},
reject: (err?: Error) => {
console.trace("reject");
reject(err);
},
};
});
return Object.assign(p, methods) as Deferred<T>;
}
export function shuffle<T>(a: T[]): T[] {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
export async function collect<T>(iter: AsyncIterable<T>): Promise<T[]> {
const buf: T[] = [];
for await (const v of iter) {
buf.push(v);
}
return buf;
}
export class Perf {
timers: Map<string, number>;
measures: Map<string, number>;
constructor() {
this.timers = new Map();
this.measures = new Map();
}
mark(key: string) {
this.timers.set(key, performance.now());
}
measure(key: string, startKey: string, endKey: string) {
const s = this.timers.get(startKey);
if (s === undefined) {
throw new Error(`${startKey} is not defined`);
}
const e = this.timers.get(endKey);
if (e === undefined) {
throw new Error(`${endKey} is not defined`);
}
this.measures.set(key, e - s);
}
getEntries(): { name: string; duration: number }[] {
const values: { name: string; duration: number }[] = [];
this.measures.forEach((v, k) => {
values.push({ name: k, duration: v });
});
return values;
}
}
export class SimpleMutex {
max: number;
current: number;
waiting: Deferred<void>[];
/**
* @param max number of concurrent operations
*/
constructor(max = 1) {
this.max = max;
this.current = 0;
this.waiting = [];
}
/**
* Returns a promise that resolves when the mutex is acquired
*/
lock(): Promise<void> {
// increment the count
this.current++;
// if we have runners, resolve it
if (this.current <= this.max) {
return Promise.resolve();
}
// otherwise defer it
const d = deferred<void>();
this.waiting.push(d);
return d;
}
/**
* Release an acquired mutex - must be called
*/
unlock(): void {
// decrement the count
this.current--;
// if we have deferred, resolve one
const d = this.waiting.pop();
d?.resolve();
}
}

@ -0,0 +1,69 @@
/*
* Copyright 2020-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ConnectionOptions,
NatsConnection,
NatsConnectionImpl,
setTransportFactory,
Transport,
TransportFactory,
} from "../nats-base-client/internal_mod";
import { WsTransport } from "./ws_transport";
export function wsUrlParseFn(u: string): string {
const ut = /^(.*:\/\/)(.*)/;
if (!ut.test(u)) {
u = `https://${u}`;
}
let url = new URL(u);
const srcProto = url.protocol.toLowerCase();
if (srcProto !== "https:" && srcProto !== "http") {
u = u.replace(/^(.*:\/\/)(.*)/gm, "$2");
url = new URL(`http://${u}`);
}
let protocol;
let port;
const host = url.hostname;
const path = url.pathname;
const search = url.search || "";
switch (srcProto) {
case "http:":
case "ws:":
case "nats:":
port = url.port || "80";
protocol = "ws:";
break;
default:
port = url.port || "443";
protocol = "wss:";
break;
}
return `${protocol}//${host}:${port}${path}${search}`;
}
export function connect(opts: ConnectionOptions = {}): Promise<NatsConnection> {
setTransportFactory({
defaultPort: 443,
urlParseFn: wsUrlParseFn,
factory: (): Transport => {
return new WsTransport();
},
} as TransportFactory);
return NatsConnectionImpl.connect(opts);
}

@ -0,0 +1,17 @@
/*
* Copyright 2020-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from "../nats-base-client/mod";
export * from "../jetstream/mod";
export { connect } from "./connect";

@ -0,0 +1,16 @@
/*
* Copyright 2020-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// this import here to drive the build system
export * from "../nats-base-client/internal_mod";

@ -0,0 +1,299 @@
/*
* Copyright 2020-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {
ConnectionOptions,
Deferred,
Server,
ServerInfo,
Transport,
} from "../nats-base-client/internal_mod";
import {
checkOptions,
DataBuffer,
deferred,
delay,
ErrorCode,
extractProtocolMessage,
INFO,
NatsError,
render,
} from "../nats-base-client/internal_mod";
const VERSION = "1.18.0";
const LANG = "nats.ws";
export type WsSocketFactory = (u: string, opts: ConnectionOptions) => Promise<{
socket: WebSocket;
encrypted: boolean;
}>;
interface WsConnectionOptions extends ConnectionOptions {
wsFactory?: WsSocketFactory;
}
export class WsTransport implements Transport {
version: string;
lang: string;
closeError?: Error;
connected: boolean;
private done: boolean;
// @ts-ignore: expecting global WebSocket
private socket: WebSocket;
private options!: WsConnectionOptions;
socketClosed: boolean;
encrypted: boolean;
peeked: boolean;
yields: Uint8Array[];
signal: Deferred<void>;
closedNotification: Deferred<void | Error>;
constructor() {
this.version = VERSION;
this.lang = LANG;
this.connected = false;
this.done = false;
this.socketClosed = false;
this.encrypted = false;
this.peeked = false;
this.yields = [];
this.signal = deferred();
this.closedNotification = deferred();
}
async connect(
server: Server,
options: WsConnectionOptions,
): Promise<void> {
const connected = false;
const connLock = deferred<void>();
// ws client doesn't support TLS setting
if (options.tls) {
connLock.reject(new NatsError("tls", ErrorCode.InvalidOption));
return connLock;
}
this.options = options;
const u = server.src;
if (options.wsFactory) {
const { socket, encrypted } = await options.wsFactory(
server.src,
options,
);
this.socket = socket;
this.encrypted = encrypted;
} else {
this.encrypted = u.indexOf("wss://") === 0;
this.socket = new WebSocket(u);
}
this.socket.binaryType = "arraybuffer";
this.socket.onopen = () => {
if (this.isDiscarded()) {
return;
}
// we don't do anything here...
};
this.socket.onmessage = (me: MessageEvent) => {
if (this.isDiscarded()) {
return;
}
this.yields.push(new Uint8Array(me.data));
if (this.peeked) {
this.signal.resolve();
return;
}
const t = DataBuffer.concat(...this.yields);
const pm = extractProtocolMessage(t);
if (pm !== "") {
const m = INFO.exec(pm);
if (!m) {
if (options.debug) {
console.error("!!!", render(t));
}
connLock.reject(new Error("unexpected response from server"));
return;
}
try {
const info = JSON.parse(m[1]) as ServerInfo;
checkOptions(info, this.options);
this.peeked = true;
this.connected = true;
this.signal.resolve();
connLock.resolve();
} catch (err) {
connLock.reject(err);
return;
}
}
};
// @ts-ignore: CloseEvent is provided in browsers
this.socket.onclose = (evt: CloseEvent) => {
if (this.isDiscarded()) {
return;
}
this.socketClosed = true;
let reason: Error | undefined;
if (this.done) return;
if (!evt.wasClean) {
reason = new Error(evt.reason);
}
this._closed(reason);
};
// @ts-ignore: signature can be any
this.socket.onerror = (e: ErrorEvent | Event): void => {
if (this.isDiscarded()) {
return;
}
const evt = e as ErrorEvent;
const err = new NatsError(
evt.message,
ErrorCode.Unknown,
new Error(evt.error),
);
if (!connected) {
connLock.reject(err);
} else {
this._closed(err);
}
};
return connLock;
}
disconnect(): void {
this._closed(undefined, true);
}
private async _closed(err?: Error, internal = true): Promise<void> {
if (this.isDiscarded()) {
return
}
if (!this.connected) return;
if (this.done) return;
this.closeError = err;
if (!err) {
while (!this.socketClosed && this.socket.bufferedAmount > 0) {
await delay(100);
}
}
this.done = true;
try {
// 1002 endpoint error, 1000 is clean
this.socket.close(err ? 1002 : 1000, err ? err.message : undefined);
} catch (err) {
// ignore this
}
if (internal) {
this.closedNotification.resolve(err);
}
}
get isClosed(): boolean {
return this.done;
}
[Symbol.asyncIterator]() {
return this.iterate();
}
async *iterate(): AsyncIterableIterator<Uint8Array> {
while (true) {
if (this.isDiscarded()) {
return
}
if (this.yields.length === 0) {
await this.signal;
}
const yields = this.yields;
this.yields = [];
for (let i = 0; i < yields.length; i++) {
if (this.options.debug) {
console.info(`> ${render(yields[i])}`);
}
yield yields[i];
}
// yielding could have paused and microtask
// could have added messages. Prevent allocations
// if possible
if (this.done) {
break;
} else if (this.yields.length === 0) {
yields.length = 0;
this.yields = yields;
this.signal = deferred();
}
}
}
isEncrypted(): boolean {
return this.connected && this.encrypted;
}
send(frame: Uint8Array): void {
if (this.isDiscarded()) {
return;
}
try {
this.socket.send(frame.buffer);
if (this.options.debug) {
console.info(`< ${render(frame)}`);
}
return;
} catch (err) {
// we ignore write errors because client will
// fail on a read or when the heartbeat timer
// detects a stale connection
if (this.options.debug) {
console.error(`!!! ${render(frame)}: ${err}`);
}
}
}
close(err?: Error | undefined): Promise<void> {
return this._closed(err, false);
}
closed(): Promise<void | Error> {
return this.closedNotification;
}
// check to see if we are discarded, as the connection
// may not have been closed, we attempt it here as well.
isDiscarded(): boolean {
if (this.done) {
this.discard();
return true
}
return false;
}
// this is to allow a force discard on a connection
// if the connection fails during the handshake protocol.
// Firefox for example, will keep connections going,
// so eventually if it succeeds, the client will have
// an additional transport running. With this
discard() {
this.done = true;
try {
this.socket?.close()
} catch (_err) {
// ignored
}
}
}

@ -0,0 +1,71 @@
/*
* Copyright 2018-2021 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Fork of https://github.com/LinusU/base32-encode
// and https://github.com/LinusU/base32-decode to support returning
// buffers without padding.
/**
* @ignore
*/
const b32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
/**
* @ignore
*/
export class base32 {
static encode(src: Uint8Array): Uint8Array {
let bits = 0;
let value = 0;
let a = new Uint8Array(src);
let buf = new Uint8Array(src.byteLength * 2);
let j = 0;
for (let i = 0; i < a.byteLength; i++) {
value = (value << 8) | a[i];
bits += 8;
while (bits >= 5) {
let index = (value >>> (bits - 5)) & 31;
buf[j++] = b32Alphabet.charAt(index).charCodeAt(0);
bits -= 5;
}
}
if (bits > 0) {
let index = (value << (5 - bits)) & 31;
buf[j++] = b32Alphabet.charAt(index).charCodeAt(0);
}
return buf.slice(0, j);
}
static decode(src: Uint8Array): Uint8Array {
let bits = 0;
let byte = 0;
let j = 0;
let a = new Uint8Array(src);
let out = new Uint8Array(a.byteLength * 5 / 8 | 0);
for (let i = 0; i < a.byteLength; i++) {
let v = String.fromCharCode(a[i]);
let vv = b32Alphabet.indexOf(v);
if (vv === -1) {
throw new Error("Illegal Base32 character: " + a[i]);
}
byte = (byte << 5) | vv;
bits += 5;
if (bits >= 8) {
out[j++] = (byte >>> (bits - 8)) & 255;
bits -= 8;
}
}
return out.slice(0, j);
}
}

@ -0,0 +1,146 @@
/*
* Copyright 2018-2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { crc16 } from "./crc16.ts";
import { NKeysError, NKeysErrorCode, Prefix, Prefixes } from "./nkeys.ts";
import { base32 } from "./base32.ts";
/**
* @ignore
*/
export interface SeedDecode {
prefix: Prefix;
buf: Uint8Array;
}
/**
* @ignore
*/
export class Codec {
static encode(prefix: Prefix, src: Uint8Array): Uint8Array {
if (!src || !(src instanceof Uint8Array)) {
throw new NKeysError(NKeysErrorCode.SerializationError);
}
if (!Prefixes.isValidPrefix(prefix)) {
throw new NKeysError(NKeysErrorCode.InvalidPrefixByte);
}
return Codec._encode(false, prefix, src);
}
static encodeSeed(role: Prefix, src: Uint8Array): Uint8Array {
if (!src) {
throw new NKeysError(NKeysErrorCode.ApiError);
}
if (!Prefixes.isValidPublicPrefix(role)) {
throw new NKeysError(NKeysErrorCode.InvalidPrefixByte);
}
if (src.byteLength !== 32) {
throw new NKeysError(NKeysErrorCode.InvalidSeedLen);
}
return Codec._encode(true, role, src);
}
static decode(expected: Prefix, src: Uint8Array): Uint8Array {
if (!Prefixes.isValidPrefix(expected)) {
throw new NKeysError(NKeysErrorCode.InvalidPrefixByte);
}
const raw = Codec._decode(src);
if (raw[0] !== expected) {
throw new NKeysError(NKeysErrorCode.InvalidPrefixByte);
}
return raw.slice(1);
}
static decodeSeed(src: Uint8Array): SeedDecode {
const raw = Codec._decode(src);
const prefix = Codec._decodePrefix(raw);
if (prefix[0] != Prefix.Seed) {
throw new NKeysError(NKeysErrorCode.InvalidSeed);
}
if (!Prefixes.isValidPublicPrefix(prefix[1])) {
throw new NKeysError(NKeysErrorCode.InvalidPrefixByte);
}
return ({ buf: raw.slice(2), prefix: prefix[1] });
}
// unsafe encode no prefix/role validation
static _encode(seed: boolean, role: Prefix, payload: Uint8Array): Uint8Array {
// offsets for this token
const payloadOffset = seed ? 2 : 1;
const payloadLen = payload.byteLength;
const checkLen = 2;
const cap = payloadOffset + payloadLen + checkLen;
const checkOffset = payloadOffset + payloadLen;
const raw = new Uint8Array(cap);
// make the prefixes human readable when encoded
if (seed) {
const encodedPrefix = Codec._encodePrefix(Prefix.Seed, role);
raw.set(encodedPrefix);
} else {
raw[0] = role;
}
raw.set(payload, payloadOffset);
//calculate the checksum write it LE
const checksum = crc16.checksum(raw.slice(0, checkOffset));
const dv = new DataView(raw.buffer);
dv.setUint16(checkOffset, checksum, true);
return base32.encode(raw);
}
// unsafe decode - no prefix/role validation
static _decode(src: Uint8Array): Uint8Array {
if (src.byteLength < 4) {
throw new NKeysError(NKeysErrorCode.InvalidEncoding);
}
let raw: Uint8Array;
try {
raw = base32.decode(src);
} catch (ex) {
throw new NKeysError(NKeysErrorCode.InvalidEncoding, ex);
}
const checkOffset = raw.byteLength - 2;
const dv = new DataView(raw.buffer);
const checksum = dv.getUint16(checkOffset, true);
const payload = raw.slice(0, checkOffset);
if (!crc16.validate(payload, checksum)) {
throw new NKeysError(NKeysErrorCode.InvalidChecksum);
}
return payload;
}
static _encodePrefix(kind: Prefix, role: Prefix): Uint8Array {
// In order to make this human printable for both bytes, we need to do a little
// bit manipulation to setup for base32 encoding which takes 5 bits at a time.
const b1 = kind | (role >> 5);
const b2 = (role & 31) << 3; // 31 = 00011111
return new Uint8Array([b1, b2]);
}
static _decodePrefix(raw: Uint8Array): Uint8Array {
// Need to do the reverse from the printable representation to
// get back to internal representation.
const b1 = raw[0] & 248; // 248 = 11111000
const b2 = (raw[0] & 7) << 5 | ((raw[1] & 248) >> 3); // 7 = 00000111
return new Uint8Array([b1, b2]);
}
}

@ -0,0 +1,298 @@
/*
* Copyright 2018-2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// An implementation of crc16 according to CCITT standards for XMODEM.
/**
* @ignore
*/
const crc16tab = new Uint16Array([
0x0000,
0x1021,
0x2042,
0x3063,
0x4084,
0x50a5,
0x60c6,
0x70e7,
0x8108,
0x9129,
0xa14a,
0xb16b,
0xc18c,
0xd1ad,
0xe1ce,
0xf1ef,
0x1231,
0x0210,
0x3273,
0x2252,
0x52b5,
0x4294,
0x72f7,
0x62d6,
0x9339,
0x8318,
0xb37b,
0xa35a,
0xd3bd,
0xc39c,
0xf3ff,
0xe3de,
0x2462,
0x3443,
0x0420,
0x1401,
0x64e6,
0x74c7,
0x44a4,
0x5485,
0xa56a,
0xb54b,
0x8528,
0x9509,
0xe5ee,
0xf5cf,
0xc5ac,
0xd58d,
0x3653,
0x2672,
0x1611,
0x0630,
0x76d7,
0x66f6,
0x5695,
0x46b4,
0xb75b,
0xa77a,
0x9719,
0x8738,
0xf7df,
0xe7fe,
0xd79d,
0xc7bc,
0x48c4,
0x58e5,
0x6886,
0x78a7,
0x0840,
0x1861,
0x2802,
0x3823,
0xc9cc,
0xd9ed,
0xe98e,
0xf9af,
0x8948,
0x9969,
0xa90a,
0xb92b,
0x5af5,
0x4ad4,
0x7ab7,
0x6a96,
0x1a71,
0x0a50,
0x3a33,
0x2a12,
0xdbfd,
0xcbdc,
0xfbbf,
0xeb9e,
0x9b79,
0x8b58,
0xbb3b,
0xab1a,
0x6ca6,
0x7c87,
0x4ce4,
0x5cc5,
0x2c22,
0x3c03,
0x0c60,
0x1c41,
0xedae,
0xfd8f,
0xcdec,
0xddcd,
0xad2a,
0xbd0b,
0x8d68,
0x9d49,
0x7e97,
0x6eb6,
0x5ed5,
0x4ef4,
0x3e13,
0x2e32,
0x1e51,
0x0e70,
0xff9f,
0xefbe,
0xdfdd,
0xcffc,
0xbf1b,
0xaf3a,
0x9f59,
0x8f78,
0x9188,
0x81a9,
0xb1ca,
0xa1eb,
0xd10c,
0xc12d,
0xf14e,
0xe16f,
0x1080,
0x00a1,
0x30c2,
0x20e3,
0x5004,
0x4025,
0x7046,
0x6067,
0x83b9,
0x9398,
0xa3fb,
0xb3da,
0xc33d,
0xd31c,
0xe37f,
0xf35e,
0x02b1,
0x1290,
0x22f3,
0x32d2,
0x4235,
0x5214,
0x6277,
0x7256,
0xb5ea,
0xa5cb,
0x95a8,
0x8589,
0xf56e,
0xe54f,
0xd52c,
0xc50d,
0x34e2,
0x24c3,
0x14a0,
0x0481,
0x7466,
0x6447,
0x5424,
0x4405,
0xa7db,
0xb7fa,
0x8799,
0x97b8,
0xe75f,
0xf77e,
0xc71d,
0xd73c,
0x26d3,
0x36f2,
0x0691,
0x16b0,
0x6657,
0x7676,
0x4615,
0x5634,
0xd94c,
0xc96d,
0xf90e,
0xe92f,
0x99c8,
0x89e9,
0xb98a,
0xa9ab,
0x5844,
0x4865,
0x7806,
0x6827,
0x18c0,
0x08e1,
0x3882,
0x28a3,
0xcb7d,
0xdb5c,
0xeb3f,
0xfb1e,
0x8bf9,
0x9bd8,
0xabbb,
0xbb9a,
0x4a75,
0x5a54,
0x6a37,
0x7a16,
0x0af1,
0x1ad0,
0x2ab3,
0x3a92,
0xfd2e,
0xed0f,
0xdd6c,
0xcd4d,
0xbdaa,
0xad8b,
0x9de8,
0x8dc9,
0x7c26,
0x6c07,
0x5c64,
0x4c45,
0x3ca2,
0x2c83,
0x1ce0,
0x0cc1,
0xef1f,
0xff3e,
0xcf5d,
0xdf7c,
0xaf9b,
0xbfba,
0x8fd9,
0x9ff8,
0x6e17,
0x7e36,
0x4e55,
0x5e74,
0x2e93,
0x3eb2,
0x0ed1,
0x1ef0,
]);
/**
* @ignore
*/
export class crc16 {
// crc16 returns the crc for the data provided.
static checksum(data: Uint8Array): number {
let crc: number = 0;
for (let i = 0; i < data.byteLength; i++) {
let b = data[i];
crc = ((crc << 8) & 0xffff) ^ crc16tab[((crc >> 8) ^ (b)) & 0x00FF];
}
return crc;
}
// validate will check the calculated crc16 checksum for data against the expected.
static validate(data: Uint8Array, expected: number): boolean {
let ba = crc16.checksum(data);
return ba == expected;
}
}

@ -0,0 +1,46 @@
/*
* Copyright 2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @ignore
*/
interface SignPair {
publicKey: Uint8Array;
secretKey: Uint8Array;
}
/**
* @ignore
*/
export interface Ed25519Helper {
fromSeed(seed: Uint8Array): SignPair;
sign(data: Uint8Array, key: Uint8Array): Uint8Array;
verify(data: Uint8Array, sig: Uint8Array, pub: Uint8Array): boolean;
randomBytes(len: number): Uint8Array;
}
/**
* @ignore
*/
let helper: Ed25519Helper;
/**
* @ignore
*/
export function setEd25519Helper(lib: Ed25519Helper) {
helper = lib;
}
/**
* @ignore
*/
export function getEd25519Helper() {
return helper;
}

@ -0,0 +1,85 @@
/*
* Copyright 2018-2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Codec } from "./codec.ts";
import { KeyPair, NKeysError, NKeysErrorCode, Prefix } from "./nkeys.ts";
import { getEd25519Helper } from "./helper.ts";
/**
* @ignore
*/
export class KP implements KeyPair {
seed?: Uint8Array;
constructor(seed: Uint8Array) {
this.seed = seed;
}
getRawSeed(): Uint8Array {
if (!this.seed) {
throw new NKeysError(NKeysErrorCode.ClearedPair);
}
let sd = Codec.decodeSeed(this.seed);
return sd.buf;
}
getSeed(): Uint8Array {
if (!this.seed) {
throw new NKeysError(NKeysErrorCode.ClearedPair);
}
return this.seed;
}
getPublicKey(): string {
if (!this.seed) {
throw new NKeysError(NKeysErrorCode.ClearedPair);
}
const sd = Codec.decodeSeed(this.seed);
const kp = getEd25519Helper().fromSeed(this.getRawSeed());
const buf = Codec.encode(sd.prefix, kp.publicKey);
return new TextDecoder().decode(buf);
}
getPrivateKey(): Uint8Array {
if (!this.seed) {
throw new NKeysError(NKeysErrorCode.ClearedPair);
}
const kp = getEd25519Helper().fromSeed(this.getRawSeed());
return Codec.encode(Prefix.Private, kp.secretKey);
}
sign(input: Uint8Array): Uint8Array {
if (!this.seed) {
throw new NKeysError(NKeysErrorCode.ClearedPair);
}
const kp = getEd25519Helper().fromSeed(this.getRawSeed());
return getEd25519Helper().sign(input, kp.secretKey);
}
verify(input: Uint8Array, sig: Uint8Array): boolean {
if (!this.seed) {
throw new NKeysError(NKeysErrorCode.ClearedPair);
}
const kp = getEd25519Helper().fromSeed(this.getRawSeed());
return getEd25519Helper().verify(input, sig, kp.publicKey);
}
clear(): void {
if (!this.seed) {
return;
}
this.seed.fill(0);
this.seed = undefined;
}
}

@ -0,0 +1,30 @@
/*
* Copyright 2020-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type { KeyPair } from "./nkeys.ts";
export {
createAccount,
createCluster,
createOperator,
createPair,
createServer,
createUser,
fromPublic,
fromSeed,
NKeysError,
NKeysErrorCode,
Prefix,
} from "./nkeys.ts";
export { decode, encode } from "./util.ts";

@ -0,0 +1,250 @@
/*
* Copyright 2018-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { KP } from "./kp.ts";
import { PublicKey } from "./public.ts";
import { Codec } from "./codec.ts";
import { getEd25519Helper } from "./helper.ts";
/**
* @ignore
*/
export function createPair(prefix: Prefix): KeyPair {
const rawSeed = getEd25519Helper().randomBytes(32);
let str = Codec.encodeSeed(prefix, new Uint8Array(rawSeed));
return new KP(str);
}
/**
* Creates a KeyPair with an operator prefix
* @returns {KeyPair} Returns the created KeyPair.
*/
export function createOperator(): KeyPair {
return createPair(Prefix.Operator);
}
/**
* Creates a KeyPair with an account prefix
* @returns {KeyPair} Returns the created KeyPair.
*/
export function createAccount(): KeyPair {
return createPair(Prefix.Account);
}
/**
* Creates a KeyPair with a user prefix
* @returns {KeyPair} Returns the created KeyPair.
*/
export function createUser(): KeyPair {
return createPair(Prefix.User);
}
/**
* @ignore
*/
export function createCluster(): KeyPair {
return createPair(Prefix.Cluster);
}
/**
* @ignore
*/
export function createServer(): KeyPair {
return createPair(Prefix.Server);
}
/**
* Creates a KeyPair from a specified public key
* @param {string} src of the public key in string format.
* @returns {KeyPair} Returns the created KeyPair.
* @see KeyPair#getPublicKey
*/
export function fromPublic(src: string): KeyPair {
const ba = new TextEncoder().encode(src);
const raw = Codec._decode(ba);
const prefix = Prefixes.parsePrefix(raw[0]);
if (Prefixes.isValidPublicPrefix(prefix)) {
return new PublicKey(ba);
}
throw new NKeysError(NKeysErrorCode.InvalidPublicKey);
}
/**
* Creates a KeyPair from a specified seed.
* @param {Uint8Array} src of the seed key as Uint8Array
* @returns {KeyPair} Returns the created KeyPair.
* @see KeyPair#getSeed
*/
export function fromSeed(src: Uint8Array): KeyPair {
Codec.decodeSeed(src);
// if we are here it decoded
return new KP(src);
}
export interface KeyPair {
/**
* Returns the public key associated with the KeyPair
* @returns {string}
* @throws NKeysError
*/
getPublicKey(): string;
/**
* Returns the private key associated with the KeyPair
* @returns Uint8Array
* @throws NKeysError
*/
getPrivateKey(): Uint8Array;
/**
* Returns the PrivateKey's seed.
* @returns Uint8Array
* @throws NKeysError
*/
getSeed(): Uint8Array;
/**
* Returns the digital signature of signing the input with the
* the KeyPair's private key.
* @param {Uint8Array} input
* @returns Uint8Array
* @throws NKeysError
*/
sign(input: Uint8Array): Uint8Array;
/**
* Returns true if the signature can be verified with the KeyPair
* @param {Uint8Array} input
* @param {Uint8Array} sig
* @returns {boolean}
* @throws NKeysError
*/
verify(input: Uint8Array, sig: Uint8Array): boolean;
/**
* Clears the secret stored in the keypair. After clearing
* a keypair cannot be used or recovered.
*/
clear(): void;
}
/**
* @ignore
*/
export enum Prefix {
//Seed is the version byte used for encoded NATS Seeds
Seed = 18 << 3, // Base32-encodes to 'S...'
//PrefixBytePrivate is the version byte used for encoded NATS Private keys
Private = 15 << 3, // Base32-encodes to 'P...'
//PrefixByteOperator is the version byte used for encoded NATS Operators
Operator = 14 << 3, // Base32-encodes to 'O...'
//PrefixByteServer is the version byte used for encoded NATS Servers
Server = 13 << 3, // Base32-encodes to 'N...'
//PrefixByteCluster is the version byte used for encoded NATS Clusters
Cluster = 2 << 3, // Base32-encodes to 'C...'
//PrefixByteAccount is the version byte used for encoded NATS Accounts
Account = 0, // Base32-encodes to 'A...'
//PrefixByteUser is the version byte used for encoded NATS Users
User = 20 << 3, // Base32-encodes to 'U...'
}
/**
* @private
*/
export class Prefixes {
static isValidPublicPrefix(prefix: Prefix): boolean {
return prefix == Prefix.Server ||
prefix == Prefix.Operator ||
prefix == Prefix.Cluster ||
prefix == Prefix.Account ||
prefix == Prefix.User;
}
static startsWithValidPrefix(s: string) {
let c = s[0];
return c == "S" || c == "P" || c == "O" || c == "N" || c == "C" ||
c == "A" || c == "U";
}
static isValidPrefix(prefix: Prefix): boolean {
let v = this.parsePrefix(prefix);
return v != -1;
}
static parsePrefix(v: number): Prefix {
switch (v) {
case Prefix.Seed:
return Prefix.Seed;
case Prefix.Private:
return Prefix.Private;
case Prefix.Operator:
return Prefix.Operator;
case Prefix.Server:
return Prefix.Server;
case Prefix.Cluster:
return Prefix.Cluster;
case Prefix.Account:
return Prefix.Account;
case Prefix.User:
return Prefix.User;
default:
return -1;
}
}
}
/**
* Possible error codes on exceptions thrown by the library.
*/
export enum NKeysErrorCode {
InvalidPrefixByte = "nkeys: invalid prefix byte",
InvalidKey = "nkeys: invalid key",
InvalidPublicKey = "nkeys: invalid public key",
InvalidSeedLen = "nkeys: invalid seed length",
InvalidSeed = "nkeys: invalid seed",
InvalidEncoding = "nkeys: invalid encoded key",
InvalidSignature = "nkeys: signature verification failed",
CannotSign = "nkeys: cannot sign, no private key available",
PublicKeyOnly = "nkeys: no seed or private key available",
InvalidChecksum = "nkeys: invalid checksum",
SerializationError = "nkeys: serialization error",
ApiError = "nkeys: api error",
ClearedPair = "nkeys: pair is cleared",
}
export class NKeysError extends Error {
name: string;
code: string;
chainedError?: Error;
/**
* @param {NKeysErrorCode} code
* @param {Error} [chainedError]
* @constructor
*
* @api private
*/
constructor(code: NKeysErrorCode, chainedError?: Error) {
super(code);
this.name = "NKeysError";
this.code = code;
this.chainedError = chainedError;
}
}

@ -0,0 +1,73 @@
/*
* Copyright 2018-2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Codec } from "./codec.ts";
import { KeyPair, NKeysError, NKeysErrorCode } from "./nkeys.ts";
import { getEd25519Helper } from "./helper.ts";
/**
* @ignore
*/
export class PublicKey implements KeyPair {
publicKey?: Uint8Array;
constructor(publicKey: Uint8Array) {
this.publicKey = publicKey;
}
getPublicKey(): string {
if (!this.publicKey) {
throw new NKeysError(NKeysErrorCode.ClearedPair);
}
return new TextDecoder().decode(this.publicKey);
}
getPrivateKey(): Uint8Array {
if (!this.publicKey) {
throw new NKeysError(NKeysErrorCode.ClearedPair);
}
throw new NKeysError(NKeysErrorCode.PublicKeyOnly);
}
getSeed(): Uint8Array {
if (!this.publicKey) {
throw new NKeysError(NKeysErrorCode.ClearedPair);
}
throw new NKeysError(NKeysErrorCode.PublicKeyOnly);
}
sign(_: Uint8Array): Uint8Array {
if (!this.publicKey) {
throw new NKeysError(NKeysErrorCode.ClearedPair);
}
throw new NKeysError(NKeysErrorCode.CannotSign);
}
verify(input: Uint8Array, sig: Uint8Array): boolean {
if (!this.publicKey) {
throw new NKeysError(NKeysErrorCode.ClearedPair);
}
let buf = Codec._decode(this.publicKey);
return getEd25519Helper().verify(input, sig, buf.slice(1));
}
clear(): void {
if (!this.publicKey) {
return;
}
this.publicKey.fill(0);
this.publicKey = undefined;
}
}

@ -0,0 +1,55 @@
/*
* Copyright 2018-2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Encode binary data to a base64 string
* @param {Uint8Array} bytes to encode to base64
*/
export function encode(bytes: Uint8Array): string {
return btoa(String.fromCharCode(...bytes));
}
/**
* Decode a base64 encoded string to a binary Uint8Array
* @param {string} b64str encoded string
*/
export function decode(b64str: string): Uint8Array {
const bin = atob(b64str);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) {
bytes[i] = bin.charCodeAt(i);
}
return bytes;
}
/**
* @ignore
*/
export function dump(buf: Uint8Array, msg?: string): void {
if (msg) {
console.log(msg);
}
let a: string[] = [];
for (let i = 0; i < buf.byteLength; i++) {
if (i % 8 === 0) {
a.push("\n");
}
let v = buf[i].toString(16);
if (v.length === 1) {
v = "0" + v;
}
a.push(v);
}
console.log(a.join(" "));
}

@ -1,5 +1,5 @@
import axios from 'axios'
import evt from './evt'
import bus from './bus'
function padLeftZero(str: string): string {
return ('00' + str).substr(str.length)
@ -56,7 +56,7 @@ const util = {
},
setToken(t: string) {
localStorage.setItem('auth_token', t)
evt.emit('token', t)
bus.emit('token', t)
},
addTokenOf(url: string) {
return url + '?auth_token=' + encodeURIComponent(this.getToken())

@ -19,7 +19,7 @@ import { modelsApp } from 'src/models';
import api from 'src/boot/api';
import msg from '@veypi/msg';
import Editor from 'src/components/editor'
import oafs from 'src/libs/oafs';
import { oafs } from '@veypi/oaer'

@ -17,7 +17,7 @@
<script lang="ts" setup>
import msg from '@veypi/msg';
import Editor from 'src/components/editor'
import oafs from 'src/libs/oafs';
import { oafs } from '@veypi/oaer';
import { computed, watch, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
let doc = ref('')

@ -15,7 +15,7 @@
<script lang="ts" setup>
import FsTree from 'src/components/FsTree.vue';
import oafs, { fileProps } from 'src/libs/oafs';
import { oafs, fileProps } from '@veypi/oaer';
import { onMounted, ref } from 'vue';
let root = ref({} as fileProps)

@ -84,6 +84,7 @@ let ifLogOut = computed(() => {
function redirect(url: string) {
console.log(url)
if (uuid.value && uuid.value !== app.id) {
api.app.get(uuid.value as string).then((app: modelsApp) => {
@ -92,9 +93,11 @@ function redirect(url: string) {
// let data = JSON.parse(Base64.decode(e.split('.')[1]))
// console.log(data)
e = encodeURIComponent(e)
console.log(e)
url = url.replaceAll('$token', e)
console.log(url)
if (url.indexOf('$token') >= 0) {
url = url.replaceAll('$token', e)
} else {
url = buildURL(url, 'token=' + e)
}
window.location.href = url
})
@ -105,10 +108,27 @@ function redirect(url: string) {
router.push({ name: 'home' })
}
}
function buildURL(url: string, params?: string) {
if (!params) {
return url;
}
// params
var hashmarkIndex = url.indexOf('#');
if (hashmarkIndex !== -1) {
url = url.slice(0, hashmarkIndex);
}
url += (url.indexOf('?') === -1 ? '?' : '&') + params;
return url;
}
onMounted(() => {
if (!ifLogOut.value && util.checkLogin()) {
redirect('')
if (ifLogOut.value) {
util.setToken('')
} else if (util.checkLogin()) {
redirect(route.query.redirect as string || '')
}
})
</script>

@ -530,20 +530,18 @@
resolved "https://registry.yarnpkg.com/@veypi/msg/-/msg-0.1.1.tgz#94864ae2c0a81991b8a30d87f12d2245fdebbead"
integrity sha512-UiAF/Y0EGT/37tGApptzHBNUpo78LbnrEkCqGAGMkJp86wrUyOgTAvuvQ197Ifqw9PIbjZM9dAgMv4DfMJQEYA==
"@veypi/oaer@^0.0.1":
version "0.0.1"
resolved "https://registry.yarnpkg.com/@veypi/oaer/-/oaer-0.0.1.tgz#b22ebaf72a7bfd5abf62f099b72b533a8cf27abd"
integrity sha512-ILY8SXK7yihH2/qhUFfPbD2FNE7O4IHh7Q9Z1A+Ild6zonCb3RM7LcOBM4/9WriEi3mPdBNmFfuoP8YFCIxHuA==
"@veypi/oaer@^0.0.3":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@veypi/oaer/-/oaer-0.0.3.tgz#b72623ae3b124ac03ca65a6715cc1a2aaa1590ee"
integrity sha512-KfbbXJFUiGoCWoYpMAKOEX32Hc2qxk0NnApCWgXAVwU5iyFC4onJpvwPn0ekbWXULWSov5T3Yde61kOirGToIw==
dependencies:
"@veypi/msg" "^0.1.0"
"@veypi/one-icon" "2"
animate.css "^4.1.1"
autoprefixer "^10.4.16"
axios "^1.5.1"
js-base64 "^3.7.5"
mitt "^3.0.1"
postcss "^8.4.31"
tailwindcss "^3.3.3"
vue "^3.3.4"
webdav "^5.3.0"
"@veypi/one-icon@2":
@ -871,15 +869,6 @@ axios@^1.2.1:
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axios@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.1.tgz#11fbaa11fc35f431193a9564109c88c1f27b585f"
integrity sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@ -3137,6 +3126,20 @@ nanoid@^3.3.6:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
nats.ws@^1.18.0:
version "1.18.0"
resolved "https://registry.yarnpkg.com/nats.ws/-/nats.ws-1.18.0.tgz#eeef911608cc3c0f0f8ff2509c8a231f40213cb5"
integrity sha512-5ITvGO2gd4vAVK733u494ysEOuorfygpig9pqqLapV3FuiFWp4APsZdtZ893zXCC5UclF9MYUzdiTj2cKPDxAA==
optionalDependencies:
nkeys.js "1.0.5"
nats@^2.17.0:
version "2.17.0"
resolved "https://registry.yarnpkg.com/nats/-/nats-2.17.0.tgz#1f6ffa5d89bb8ea4549e205f42601dbf66c4561e"
integrity sha512-749TtweWL6bc9R9yNra4a+tuk8J0bqurxcPV/9R2D+WPTplY4PPde/LPSXspqR/eCCTxiM80/AjVlfboEafRxA==
dependencies:
nkeys.js "1.0.5"
natural-compare-lite@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4"
@ -3157,6 +3160,13 @@ nested-property@^4.0.0:
resolved "https://registry.yarnpkg.com/nested-property/-/nested-property-4.0.0.tgz#a67b5a31991e701e03cdbaa6453bc5b1011bb88d"
integrity sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==
nkeys.js@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nkeys.js/-/nkeys.js-1.0.5.tgz#3024bde671eb33be0316ff2d5abe8b8cec960158"
integrity sha512-u25YnRPHiGVsNzwyHnn+PT90sgAhnS8jUJ1nxmkHMFYCJ6+Ic0lv291w7uhRBpJVJ3PH2GWbYqA151lGCRrB5g==
dependencies:
tweetnacl "1.0.3"
no-case@^2.2.0:
version "2.3.2"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
@ -4116,6 +4126,13 @@ ts-interface-checker@^0.1.9:
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
ts-nkeys@^1.0.16:
version "1.0.16"
resolved "https://registry.yarnpkg.com/ts-nkeys/-/ts-nkeys-1.0.16.tgz#b0c6e7c4f16f976c7e7ddb6982fc789a2f971248"
integrity sha512-1qrhAlavbm36wtW+7NtKOgxpzl+70NTF8xlz9mEhiA5zHMlMxjj3sEVKWm3pGZhHXE0Q3ykjrj+OSRVaYw+Dqg==
dependencies:
tweetnacl "^1.0.3"
tslib@^1.8.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@ -4133,6 +4150,11 @@ tsutils@^3.21.0:
dependencies:
tslib "^1.8.1"
tweetnacl@1.0.3, tweetnacl@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@ -4294,7 +4316,7 @@ vue-router@^4.0.0:
dependencies:
"@vue/devtools-api" "^6.5.0"
vue@^3.0.0, vue@^3.2.20, vue@^3.3.4:
vue@^3.0.0, vue@^3.2.20:
version "3.3.4"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.4.tgz#8ed945d3873667df1d0fcf3b2463ada028f88bd6"
integrity sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==

@ -0,0 +1,21 @@
# nats cfg
#
host: 127.0.0.1
port: 4222
# 监控端口
http_port: 8222
jetstream: {
}
authorization: {
users: [
{ nkey: UCXFAAVMCPTATZUZX6H24YF6FI3NKPQBPLM6BNN2EDFPNSUUEZPNFKEL}
]
}
websocket: {
listen: '0.0.0.0:4221'
no_tls: true
}
Loading…
Cancel
Save