From 4548a45ecb9c75c97127a15cb0b6a0eba229b2c1 Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 22 Jul 2025 17:19:39 +0400 Subject: [PATCH 01/29] Add docs for v1 --- .gitignore | 2 + docs/.eslintignore | 6 + docs/.eslintrc.cjs | 13 + docs/.node-version | 1 + docs/.vuepress/client.ts | 10 + docs/.vuepress/config.ts | 28 + docs/.vuepress/configs/theme.ts | 36 + docs/.vuepress/lib/externalPlaceholders.ts | 6 + docs/.vuepress/lib/log.ts | 12 + docs/.vuepress/lib/types.ts | 18 + docs/.vuepress/lib/version.ts | 15 + docs/.vuepress/lib/versioning.ts | 135 + docs/.vuepress/markdown/linkCheck/index.ts | 90 + docs/.vuepress/markdown/replaceLink/index.ts | 47 + docs/.vuepress/markdown/resolver.ts | 30 + docs/.vuepress/markdown/types.ts | 40 + .../xode/createImportCodeBlockRule.ts | 106 + .../markdown/xode/importCodePlugin.ts | 45 + .../markdown/xode/normalizeWhitespace.ts | 14 + .../markdown/xode/resolveImportCode.ts | 92 + docs/.vuepress/markdown/xode/types.ts | 15 + .../.vuepress/public/Kurrent Logo - Black.svg | 26 + docs/.vuepress/public/Kurrent Logo - Plum.svg | 26 + .../.vuepress/public/Kurrent Logo - White.svg | 26 + docs/.vuepress/public/cloud.png | Bin 0 -> 247451 bytes docs/.vuepress/public/favicon.ico | Bin 0 -> 15406 bytes docs/.vuepress/public/favicon.png | Bin 0 -> 1614 bytes docs/.vuepress/public/fonts/Solina-Bold.woff | Bin 0 -> 17888 bytes docs/.vuepress/public/fonts/Solina-Bold.woff2 | Bin 0 -> 16364 bytes docs/.vuepress/public/fonts/Solina-Light.woff | Bin 0 -> 17804 bytes .../.vuepress/public/fonts/Solina-Light.woff2 | Bin 0 -> 16344 bytes .../.vuepress/public/fonts/Solina-Medium.woff | Bin 0 -> 17684 bytes .../public/fonts/Solina-Medium.woff2 | Bin 0 -> 16148 bytes .../public/fonts/Solina-Regular.woff | Bin 0 -> 17800 bytes .../public/fonts/Solina-Regular.woff2 | Bin 0 -> 16312 bytes docs/.vuepress/public/js/snippet.js | 86 + docs/.vuepress/public/logo-white.png | Bin 0 -> 15352 bytes docs/.vuepress/public/robots.txt | 15 + docs/.vuepress/public/video.png | Bin 0 -> 8640 bytes docs/.vuepress/styles/index.scss | 139 + docs/.vuepress/styles/palette.scss | 193 + docs/api/appending-events.md | 193 + docs/api/authentication.md | 47 + docs/api/delete-stream.md | 36 + docs/api/getting-started.md | 137 + docs/api/images/duplicate-event.png | Bin 0 -> 26730 bytes docs/api/observability.md | 150 + docs/api/persistent-subscriptions.md | 226 + docs/api/projections.md | 389 + docs/api/reading-events.md | 193 + docs/api/subscriptions.md | 478 + docs/package.json | 54 + docs/pnpm-lock.yaml | 9767 +++++++++++++++++ docs/tsconfig.json | 13 + 54 files changed, 12955 insertions(+) create mode 100644 docs/.eslintignore create mode 100644 docs/.eslintrc.cjs create mode 100644 docs/.node-version create mode 100644 docs/.vuepress/client.ts create mode 100644 docs/.vuepress/config.ts create mode 100644 docs/.vuepress/configs/theme.ts create mode 100644 docs/.vuepress/lib/externalPlaceholders.ts create mode 100644 docs/.vuepress/lib/log.ts create mode 100644 docs/.vuepress/lib/types.ts create mode 100644 docs/.vuepress/lib/version.ts create mode 100644 docs/.vuepress/lib/versioning.ts create mode 100644 docs/.vuepress/markdown/linkCheck/index.ts create mode 100644 docs/.vuepress/markdown/replaceLink/index.ts create mode 100644 docs/.vuepress/markdown/resolver.ts create mode 100644 docs/.vuepress/markdown/types.ts create mode 100644 docs/.vuepress/markdown/xode/createImportCodeBlockRule.ts create mode 100644 docs/.vuepress/markdown/xode/importCodePlugin.ts create mode 100644 docs/.vuepress/markdown/xode/normalizeWhitespace.ts create mode 100644 docs/.vuepress/markdown/xode/resolveImportCode.ts create mode 100644 docs/.vuepress/markdown/xode/types.ts create mode 100644 docs/.vuepress/public/Kurrent Logo - Black.svg create mode 100644 docs/.vuepress/public/Kurrent Logo - Plum.svg create mode 100644 docs/.vuepress/public/Kurrent Logo - White.svg create mode 100644 docs/.vuepress/public/cloud.png create mode 100644 docs/.vuepress/public/favicon.ico create mode 100644 docs/.vuepress/public/favicon.png create mode 100644 docs/.vuepress/public/fonts/Solina-Bold.woff create mode 100644 docs/.vuepress/public/fonts/Solina-Bold.woff2 create mode 100644 docs/.vuepress/public/fonts/Solina-Light.woff create mode 100644 docs/.vuepress/public/fonts/Solina-Light.woff2 create mode 100644 docs/.vuepress/public/fonts/Solina-Medium.woff create mode 100644 docs/.vuepress/public/fonts/Solina-Medium.woff2 create mode 100644 docs/.vuepress/public/fonts/Solina-Regular.woff create mode 100644 docs/.vuepress/public/fonts/Solina-Regular.woff2 create mode 100644 docs/.vuepress/public/js/snippet.js create mode 100644 docs/.vuepress/public/logo-white.png create mode 100644 docs/.vuepress/public/robots.txt create mode 100644 docs/.vuepress/public/video.png create mode 100644 docs/.vuepress/styles/index.scss create mode 100644 docs/.vuepress/styles/palette.scss create mode 100644 docs/api/appending-events.md create mode 100644 docs/api/authentication.md create mode 100644 docs/api/delete-stream.md create mode 100644 docs/api/getting-started.md create mode 100644 docs/api/images/duplicate-event.png create mode 100644 docs/api/observability.md create mode 100644 docs/api/persistent-subscriptions.md create mode 100644 docs/api/projections.md create mode 100644 docs/api/reading-events.md create mode 100644 docs/api/subscriptions.md create mode 100644 docs/package.json create mode 100644 docs/pnpm-lock.yaml create mode 100644 docs/tsconfig.json diff --git a/.gitignore b/.gitignore index a8fb4ffc4..b21b5418b 100644 --- a/.gitignore +++ b/.gitignore @@ -365,3 +365,5 @@ certs/ certs-cluster/ .DS_Store docs/public + +.temp \ No newline at end of file diff --git a/docs/.eslintignore b/docs/.eslintignore new file mode 100644 index 000000000..f4e822b6f --- /dev/null +++ b/docs/.eslintignore @@ -0,0 +1,6 @@ +!.vuepress/ +!.*.js +.cache/ +.temp/ +node_modules/ +dist/ \ No newline at end of file diff --git a/docs/.eslintrc.cjs b/docs/.eslintrc.cjs new file mode 100644 index 000000000..92de49d89 --- /dev/null +++ b/docs/.eslintrc.cjs @@ -0,0 +1,13 @@ +module.exports = { + root: true, + extends: 'vuepress', + overrides: [ + { + files: ['*.ts', '*.vue'], + extends: 'vuepress-typescript', + parserOptions: { + project: ['tsconfig.json'], + }, + }, + ], +} \ No newline at end of file diff --git a/docs/.node-version b/docs/.node-version new file mode 100644 index 000000000..2edeafb09 --- /dev/null +++ b/docs/.node-version @@ -0,0 +1 @@ +20 \ No newline at end of file diff --git a/docs/.vuepress/client.ts b/docs/.vuepress/client.ts new file mode 100644 index 000000000..063caa7a7 --- /dev/null +++ b/docs/.vuepress/client.ts @@ -0,0 +1,10 @@ +import type {Router} from "vue-router"; + +interface ClientConfig { + enhance?: (context: { + app: any; + router: Router; + siteData: any; + }) => void | Promise; + setup?: () => void; +} \ No newline at end of file diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts new file mode 100644 index 000000000..b16f12a96 --- /dev/null +++ b/docs/.vuepress/config.ts @@ -0,0 +1,28 @@ +import { dl } from "@mdit/plugin-dl"; +import viteBundler from "@vuepress/bundler-vite"; +import vueDevTools from 'vite-plugin-vue-devtools' +import { defineUserConfig } from "vuepress"; +import { hopeTheme } from "vuepress-theme-hope"; +import { themeOptions } from "./configs/theme"; +import { linkCheckPlugin } from "./markdown/linkCheck"; +import { replaceLinkPlugin } from "./markdown/replaceLink"; + + +export default defineUserConfig({ + base: "/", + dest: "public", + title: "Docs", + description: "Event-native database", + bundler: viteBundler({ viteOptions: { plugins: [vueDevTools(),], } }), + markdown: { importCode: false }, + extendsMarkdown: md => { + md.use(linkCheckPlugin); + md.use(replaceLinkPlugin, { + replaceLink: (link: string, _) => link + .replace("@api", "/api") + .replace("@server/", "/server/{version}/") + }); + md.use(dl); + }, + theme: hopeTheme(themeOptions, { custom: true }), +}); diff --git a/docs/.vuepress/configs/theme.ts b/docs/.vuepress/configs/theme.ts new file mode 100644 index 000000000..41aefcdfb --- /dev/null +++ b/docs/.vuepress/configs/theme.ts @@ -0,0 +1,36 @@ +import type {ThemeOptions} from "vuepress-theme-hope"; + +export const themeOptions: ThemeOptions = { + logo: "/Kurrent Logo - Plum.svg", + logoDark: "/Kurrent Logo - White.svg", + docsDir: 'docs', + editLink: false, + lastUpdated: true, + toc: true, + repo: "https://github.com/kurrent-io", + repoLabel: "GitHub", + repoDisplay: true, + contributors: false, + pure: false, + darkmode:"toggle", + headerDepth: 3, + pageInfo: false, + markdown: { + figure: true, + imgLazyload: true, + imgMark: true, + imgSize: true, + tabs: true, + codeTabs: true, + component: true, + mermaid: true, + highlighter: { + type: "shiki", + themes: { + light: "one-light", + dark: "one-dark-pro", + } + } + } +} + diff --git a/docs/.vuepress/lib/externalPlaceholders.ts b/docs/.vuepress/lib/externalPlaceholders.ts new file mode 100644 index 000000000..052bc196d --- /dev/null +++ b/docs/.vuepress/lib/externalPlaceholders.ts @@ -0,0 +1,6 @@ +const externalPlaceholders = ["@samples", "@clients"]; + +export function isKnownPlaceholder(placeholder: string): boolean { + const known = externalPlaceholders.find(x => x == placeholder); + return known !== undefined; +} diff --git a/docs/.vuepress/lib/log.ts b/docs/.vuepress/lib/log.ts new file mode 100644 index 000000000..92b3140b2 --- /dev/null +++ b/docs/.vuepress/lib/log.ts @@ -0,0 +1,12 @@ +export default { + error(message: string) { + console.log("\x1b[41m%s\x1b[0m", ' ERROR ', `${message}\n`) + process.exit(0) + }, + info(message: string) { + console.log("\x1b[44m%s\x1b[0m", ' INFO ', `${message}\n`) + }, + success(message: string) { + console.log("\x1b[42m\x1b[30m%s\x1b[0m", ' DONE ', `${message}\n`) + } +} diff --git a/docs/.vuepress/lib/types.ts b/docs/.vuepress/lib/types.ts new file mode 100644 index 000000000..352054078 --- /dev/null +++ b/docs/.vuepress/lib/types.ts @@ -0,0 +1,18 @@ +import type {NavItemOptions, SidebarLinkOptions} from "vuepress-theme-hope"; + +export interface EsSidebarGroupOptions extends NavItemOptions { + group?: string; + collapsible?: boolean; + title?: string; + version?: string; + prefix?: string; + link?: string; + children: EsSidebarItemOptions[] | string[]; +} + +export type EsSidebarItemOptions = SidebarLinkOptions | EsSidebarGroupOptions | string; +export type EsSidebarArrayOptions = EsSidebarItemOptions[]; +export type EsSidebarObjectOptions = Record; +export type EsSidebarOptions = EsSidebarArrayOptions | EsSidebarObjectOptions; + +export type ImportedSidebarArrayOptions = EsSidebarGroupOptions[]; \ No newline at end of file diff --git a/docs/.vuepress/lib/version.ts b/docs/.vuepress/lib/version.ts new file mode 100644 index 000000000..92fcbf59d --- /dev/null +++ b/docs/.vuepress/lib/version.ts @@ -0,0 +1,15 @@ +const versionRegex = /v((\d+\.)?(\d+\.)?(\*|\d+))/; +const nightly = "nightly"; +const v = { + isVersion: (v: string) => versionRegex.test(v), + parseVersion: (v: string) => versionRegex.exec(v), + getVersion: (path: string): string | undefined => { + if (path.includes(nightly)) { + return nightly; + } + const ref = path.split("#")[0]; + const split = ref.split("/"); + return split.find(x => v.isVersion(x)); + } +}; +export default v; diff --git a/docs/.vuepress/lib/versioning.ts b/docs/.vuepress/lib/versioning.ts new file mode 100644 index 000000000..ea5eb0334 --- /dev/null +++ b/docs/.vuepress/lib/versioning.ts @@ -0,0 +1,135 @@ +import * as fs from "fs"; +import {path} from 'vuepress/utils'; +import log from "./log"; +import {createRequire} from 'node:module'; +// import references from "../versions.json"; +import type { + EsSidebarGroupOptions, EsSidebarObjectOptions, + ImportedSidebarArrayOptions +} from "./types"; + +interface VersionDetail { + version: string, + path: string, + startPage: string +} + +interface Version { + id: string, + group: string, + basePath: string, + versions: VersionDetail[] +} + +const createSidebarItem = (item: EsSidebarGroupOptions, path: string, version: string, group: string): EsSidebarGroupOptions => { + const xp = `/${path}/`; + let ch = item.children as string[]; + if (item.collapsible !== undefined) { + ch = ch.map(x => !x.startsWith('../') ? '../' + x : x); + } + const childPath = item.prefix ? `/${path}${item.prefix}` : xp; + const children = ch.map(x => x.split(xp).join("")); + return { + ...item, + children: children.map(x => `${childPath}${x}`), + prefix: undefined, + group, + version, + text: item.text || item.title || "" + } +} + +export class versioning { + versions: Version[] = []; + + constructor() { + // const require = createRequire(import.meta.url) + // references.forEach(p => { + // const fileName = path.resolve(__dirname, p); + // if (fs.existsSync(fileName)) { + // log.info(`Importing versions from ${fileName}`); + // const list: Version[] = require(fileName); + // list.forEach(v => { + // const existing = this.versions.find(x => x.id === v.id); + // if (existing === undefined) { + // this.versions.push(v); + // } else { + // existing.versions.push(...v.versions); + // } + // }); + // } else { + // log.info(`File ${fileName} doesn't exist, ignoring`); + // } + // }); + } + + get latestSemver(): string { + const serverDocs = this.versions.find(v => v.id === "server"); + if (!serverDocs) { + throw new Error("Server docs not found"); + } + return serverDocs.versions[0].path; + } + + // latest stable release + get latest(): string { + const serverDocs = this.versions.find(v => v.id === "server"); + if (!serverDocs) { + throw new Error("Server docs not found"); + } + return `${serverDocs.basePath}/${serverDocs.versions[0].path}`; + } + + get all() { + return this.versions + } + + // Generate a single object that represents all versions from each sidebar + getSidebars() { + let sidebars: EsSidebarObjectOptions = {}; + const require = createRequire(import.meta.url); + + this.versions.forEach(version => { + version.versions.forEach(v => { + const p = `${version.basePath}/${v.path}`; + const sidebarPath = path.resolve(__dirname, `../../${p}`); + const sidebarBase = path.join(sidebarPath, "sidebar"); + const sidebarJs = `${sidebarBase}.js`; + const sidebarCjs = `${sidebarBase}.cjs`; + fs.copyFileSync(sidebarJs, sidebarCjs); + log.info(`Importing sidebar from ${sidebarJs}`); + const sidebar: ImportedSidebarArrayOptions = require(sidebarCjs); + sidebars[`/${p}/`] = sidebar.map(item => createSidebarItem(item, p, v.version, version.group)); + fs.rmSync(sidebarCjs); + }); + }) + + console.log(JSON.stringify(sidebars, null, 2)); + return sidebars; + } + + version(id: string) { + const ret = this.versions.find(x => x.id === id); + if (ret === undefined) log.error(`Version ${id} not defined`); + return ret; + } + + // Build dropdown items for each version + linksFor(id: string, url?: string) { + const links: { text: string, link: string }[] = []; + const version = this.version(id); + if (version === undefined) return links; + + version.versions.forEach(v => { + const path = `${version.basePath}/${v.path}`; + const pageUrl = (url ? url : v.startPage ? v.startPage : ""); + const link = `/${path}/${pageUrl}`; + const item = {text: v.version, link: link}; + links.push(item); + }); + + return links; + } +} + +export const instance: versioning = new versioning(); diff --git a/docs/.vuepress/markdown/linkCheck/index.ts b/docs/.vuepress/markdown/linkCheck/index.ts new file mode 100644 index 000000000..e6233b646 --- /dev/null +++ b/docs/.vuepress/markdown/linkCheck/index.ts @@ -0,0 +1,90 @@ +import {type PluginSimple} from "markdown-it"; +import {fs, logger, path} from "vuepress/utils"; +import type {MarkdownEnv, MdToken} from "../types"; + +function findAnchor(filename: string, anchor: string): boolean { + const asAnchor = (header: string) => header + .replace(/[^\w\s\-']/gi, "") + .trim() + .toLowerCase() + // @ts-ignore + .replaceAll(" ", "-") + .replaceAll("'", "-"); + + // const enableLogs = filename.endsWith("v24.6/operations.md"); + + const href = ``; + const lines = fs.readFileSync(filename).toString().split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.includes(href)) return true; + + if (line.charAt(0) != "#") continue; + + const lineAnchor = asAnchor(line); + // if (enableLogs) logger.tip(lineAnchor) + if (lineAnchor === anchor || lineAnchor.replace("--", "-") === anchor) return true; + } + return false; +} + +function checkLink(token: MdToken, attrName: string, env: MarkdownEnv) { + const href = token.attrGet(attrName); + if (href === null) return; + if (href.startsWith("http") || href.endsWith(".html")) return; + ensureLocalLink(href, env, true); +} + +export function ensureLocalLink(href: string, env: MarkdownEnv, ignorePlaceholders: boolean) { + if (ignorePlaceholders && (href.startsWith("@") || env.filePathRelative!.startsWith("samples"))) return; + // check the link + const split = href.split("#"); + const currentPath = href[0] == "/" ? path.resolve(__dirname, "../../..") : path.dirname(env.filePath!); + const p = path.join(currentPath, split[0]); + fs.stat(p, (err, stat) => { + if (err != null) { + logger.error(`Broken link in ${env.filePathRelative}\r\nto: ${split[0]}`); + return; + } + let pathToCheck = p; + if (stat.isDirectory()) { + if (split[0] !== "") { + logger.error(`Link to directory in ${env.filePathRelative}\r\nto: ${href}`); + return; + } + pathToCheck = env.filePath!; + } + if (split.length > 1) { + const anchorResolved = findAnchor(pathToCheck, split[1]); + if (!anchorResolved) { + logger.error(`Broken anchor link in ${env.filePathRelative}: ${split[1]} in file ${pathToCheck}`); + } + } + }); +} + +export const linkCheckPlugin: PluginSimple = (md) => { + md.core.ruler.after( + "inline", + "link-check", + (state) => { + state.tokens.forEach((blockToken) => { + if (!(blockToken.type === "inline" && blockToken.children)) { + return; + } + + blockToken.children.forEach((token) => { + const type = token.type; + switch (type) { + case "link_open": + checkLink(token, "href", state.env); + break; + case "image": + checkLink(token, "src", state.env) + break; + } + }) + }) + } + ) +} diff --git a/docs/.vuepress/markdown/replaceLink/index.ts b/docs/.vuepress/markdown/replaceLink/index.ts new file mode 100644 index 000000000..7bc0e856a --- /dev/null +++ b/docs/.vuepress/markdown/replaceLink/index.ts @@ -0,0 +1,47 @@ +import {type PluginWithOptions} from "markdown-it"; +import {logger} from "vuepress/utils"; +import {isKnownPlaceholder} from "../../lib/externalPlaceholders"; +import type {MarkdownEnv, MdToken} from "../types"; + +export interface ReplaceLinkPluginOptions { + replaceLink?: (link: string, env: any) => string; +} + +function replaceCrossLinks(token: MdToken, env: MarkdownEnv) { + const href = token.attrGet("href"); + if (href === null) return; + if (!href.startsWith("@")) return; + + const placeholder = href.split("/")[0]; + const known = isKnownPlaceholder(placeholder); + + if (!known) { + logger.error(`Unable to resolve placeholder ${placeholder} in ${href}, file ${env.filePathRelative}`); + return; + } +} + +export const replaceLinkPlugin: PluginWithOptions = (md, opts) => { + md.core.ruler.after( + "inline", + "replace-link", + (state) => { + if (opts?.replaceLink === undefined) return; + state.tokens.forEach((blockToken) => { + if (!(blockToken.type === "inline" && blockToken.children)) { + return; + } + + const replaceAttr = (token: MdToken, attrName: string) => token.attrSet(attrName, opts.replaceLink!(token.attrGet(attrName)!, state.env)); + + blockToken.children.forEach((token) => { + const type = token.type; + if (type === "link_open") { + replaceAttr(token, "href"); + replaceCrossLinks(token, state.env); + } + }); + }); + } + ) +} diff --git a/docs/.vuepress/markdown/resolver.ts b/docs/.vuepress/markdown/resolver.ts new file mode 100644 index 000000000..b70786954 --- /dev/null +++ b/docs/.vuepress/markdown/resolver.ts @@ -0,0 +1,30 @@ +import {logger, path} from "vuepress/utils"; +import version from "../lib/version"; +import {instance} from "../lib/versioning"; + +export const resolveVersionedPath = (importPath: string, filePath: string | null | undefined) => { + let importFilePath = importPath; + let error: string | null = null; + + if (!path.isAbsolute(importPath)) { + // if the importPath is a relative path, we need to resolve it + // according to the markdown filePath + if (!filePath) { + logger.error(`Unable to resolve code path: ${filePath}`); + return { + importFilePath: null, + error: 'Error when resolving path', + }; + } + importFilePath = path.resolve(filePath, '..', importPath); + } + + // if (importFilePath.includes("{version}")) { + // const ver = version.getVersion(filePath!) ?? instance.latestSemver; + // if (ver) { + // importFilePath = importFilePath.replace("{version}", ver); + // } + // } + + return {importFilePath, error}; +} \ No newline at end of file diff --git a/docs/.vuepress/markdown/types.ts b/docs/.vuepress/markdown/types.ts new file mode 100644 index 000000000..dfcab2255 --- /dev/null +++ b/docs/.vuepress/markdown/types.ts @@ -0,0 +1,40 @@ +import type {PageFrontmatter, PageHeader} from "vuepress"; + +export type MarkdownHeader = PageHeader; + +export interface MarkdownLink { + raw: string; + relative: string; + absolute: string; +} + +export interface MarkdownEnv { + base?: string; + filePath?: string | null; + filePathRelative?: string | null; + frontmatter?: PageFrontmatter; + headers?: MarkdownHeader[]; + hoistedTags?: string[]; + importedFiles?: string[]; + links?: MarkdownLink[]; + title?: string; +} + +type Nesting = 1 | 0 | -1; + +export interface MdToken { + type: string; + tag: string; + attrs: Array<[string, string]> | null; + map: [number, number] | null; + nesting: Nesting; + level: number; + attrSet(name: string, value: string): void; + attrGet(name: string): string | null; +} + +export interface MdEnv { + base: string; + filePath: string; + filePathRelative: string; +} diff --git a/docs/.vuepress/markdown/xode/createImportCodeBlockRule.ts b/docs/.vuepress/markdown/xode/createImportCodeBlockRule.ts new file mode 100644 index 000000000..69fdaf01c --- /dev/null +++ b/docs/.vuepress/markdown/xode/createImportCodeBlockRule.ts @@ -0,0 +1,106 @@ +import {path} from "vuepress/utils" +import type {ExtendedCodeImportPluginOptions, ImportCodeTokenMeta, ResolvedImport} from "./types"; +// @ts-ignore +import type {RuleBlock} from "markdown-it/lib/parser_block"; +// @ts-ignore +import type {StateBlock} from "markdown-it"; + +// min length of the import code syntax, i.e. '@[code]()' +const MIN_LENGTH = 9; + +const startSequence = "@[code"; + +const knownPrismIssues: Record = { + "rs": "rust" +}; + +const replaceKnownPrismExtensions = (ext: string): string => knownPrismIssues[ext] ?? ext; + +// regexp to match the import syntax +const SYNTAX_RE = /^@\[code(?:{(\d+)?-(\d+)?})?(?:{(.+)?})?(?: ([^\]]+))?]\(([^)]*)\)/; + +const name = "tabs"; + +export const createImportCodeBlockRule = ({ + handleImportPath = (str) => [{importPath: str}], + }: ExtendedCodeImportPluginOptions): RuleBlock => ( + state: StateBlock, + startLine: number, + endLine: number, + silent: boolean +): boolean => { + // if it's indented more than 3 spaces, it should be a code block + /* istanbul ignore if */ + if (state.sCount[startLine] - state.blkIndent >= 4) { + return false; + } + + const pos = state.bMarks[startLine] + state.tShift[startLine]; + const max = state.eMarks[startLine]; + + // return false if the length is shorter than min length + if (pos + MIN_LENGTH > max) return false; + + // check if it's matched the start + if (state.src.substr(pos, startSequence.length) !== startSequence) return false; + + // check if it's matched the syntax + const match = state.src.slice(pos, max).match(SYNTAX_RE); + if (!match) return false; + + // return true as we have matched the syntax + if (silent) return true; + + const [, lineStart, lineEnd, region, info, importPath] = match; + + const resolvedImports = handleImportPath(importPath); + + const addCodeBlock = (r: ResolvedImport) => { + const meta: ImportCodeTokenMeta = { + importPath: r.importPath, + lineStart: lineStart ? Number.parseInt(lineStart, 10) : 0, + lineEnd: lineEnd ? Number.parseInt(lineEnd, 10) : undefined, + region: region + }; + + // create an import_code token + const token = state.push('import_code', 'code', 0); + + // use user specified info, or fallback to file ext + token.info = info ?? replaceKnownPrismExtensions(path.extname(meta.importPath).slice(1)); + token.markup = '```'; + token.map = [startLine, startLine + 1]; + // store token meta to be used in renderer rule + token.meta = meta; + } + + const addGroupItem = (r: ResolvedImport) => { + const token = state.push(`${name}_tab_open`, "", 1); + token.block = true; + token.info = r.label; + token.meta = {active: false}; + + addCodeBlock(r); + + state.push(`${name}_tab_close`, "", -1); + } + + const experiment = resolvedImports.length > 1; + if (experiment) { + const token = state.push(`${name}_tabs_open`, "", 1); + token.info = name; + token.meta = {id: "code"}; + + for (const resolved of resolvedImports) { + addGroupItem(resolved); + } + + state.push(`${name}_tabs_close`, "", -1); + } else { + addCodeBlock(resolvedImports[0]); + } + + state.line = startLine + 1; + + return true; +} diff --git a/docs/.vuepress/markdown/xode/importCodePlugin.ts b/docs/.vuepress/markdown/xode/importCodePlugin.ts new file mode 100644 index 000000000..d3bf50dee --- /dev/null +++ b/docs/.vuepress/markdown/xode/importCodePlugin.ts @@ -0,0 +1,45 @@ +import type {PluginWithOptions} from "markdown-it"; +import type {MarkdownEnv} from "../types"; +import {resolveImportCode} from "./resolveImportCode"; +import {createImportCodeBlockRule} from "./createImportCodeBlockRule"; +import {type ExtendedCodeImportPluginOptions} from "./types"; +import { normalizeWhitespace } from "./normalizeWhitespace"; + +export const importCodePlugin: PluginWithOptions = ( + md, + options = {} +): void => { + // add import_code block rule + md.block.ruler.before( + 'fence', + 'import_code', + createImportCodeBlockRule(options), + { + alt: ['paragraph', 'reference', 'blockquote', 'list'], + } + ); + + // add import_code renderer rule + md.renderer.rules.import_code = ( + tokens, + idx, + options, + env: MarkdownEnv, + slf + ) => { + const token = tokens[idx]; + + // use imported code as token content + const {importFilePath, importCode} = resolveImportCode(token.meta, env); + token.content = normalizeWhitespace(importCode); + + // extract imported files to env + if (importFilePath) { + const importedFiles = env.importedFiles || (env.importedFiles = []); + importedFiles.push(importFilePath); + } + + // render the import_code token as a fence token + return md.renderer.rules.fence!(tokens, idx, options, env, slf); + } +} diff --git a/docs/.vuepress/markdown/xode/normalizeWhitespace.ts b/docs/.vuepress/markdown/xode/normalizeWhitespace.ts new file mode 100644 index 000000000..a4127f6a6 --- /dev/null +++ b/docs/.vuepress/markdown/xode/normalizeWhitespace.ts @@ -0,0 +1,14 @@ +export const normalizeWhitespace = (input: string): string => { + // empty lines before start of snippet + const trimmed = input.replace(/^([ ]*\n)*/, ''); + + // find the shortest indent at the start of line + const shortest = trimmed.match(/^[ ]*(?=\S)/gm)?.reduce((a, b) => a.length <= b.length ? a : b) ?? ''; + + if (shortest.length) { + // remove the shortest indent from each line (if exists) + return trimmed.replace(new RegExp(`^${shortest}`, 'gm'), ''); + } + + return trimmed; +} diff --git a/docs/.vuepress/markdown/xode/resolveImportCode.ts b/docs/.vuepress/markdown/xode/resolveImportCode.ts new file mode 100644 index 000000000..93b8f5659 --- /dev/null +++ b/docs/.vuepress/markdown/xode/resolveImportCode.ts @@ -0,0 +1,92 @@ +import {fs} from "vuepress/utils"; +import type {MarkdownEnv} from "../types"; +import type {ImportCodeTokenMeta} from "./types"; +import {resolveVersionedPath} from "../resolver"; + +function testLine(line: string, regexp: RegExp, regionName: string, end = false) { + const [full, tag, name] = regexp.exec(line.trim()) || []; + return ( + full + && tag + && name === regionName + && tag.match(end ? /^[Ee]nd ?[rR]egion$/ : /^[rR]egion$/) + ); +} + +function findRegion(lines: string[] | null, regionName: string) { + if (lines === null) return undefined; + const regionRegexps = [ + /^\/\/ ?#?((?:end)?region) ([\w*-]+)$/, // javascript, typescript, java, go + /^\/\* ?#((?:end)?region) ([\w*-]+) ?\*\/$/, // css, less, scss + /^#pragma ((?:end)?region) ([\w*-]+)$/, // C, C++ + /^$/, // HTML, markdown + /^#(End Region) ([\w*-]+)$/, // Visual Basic + /^::#(endregion) ([\w*-]+)$/, // Bat + /^# ?((?:end)?region) ([\w*-]+)$/ // C#, PHP, PowerShell, Python, perl & misc + ]; + + let regexp = null; + let start = -1; + + for (const [lineId, line] of lines.entries()) { + if (regexp === null) { + for (const reg of regionRegexps) { + if (testLine(line, reg, regionName)) { + start = lineId + 1; + regexp = reg; + break; + } + } + } else if (testLine(line, regexp, regionName, true)) { + return {start, end: lineId, regexp}; + } + } + + return null; +} + +export const resolveImportCode = ( + {importPath, lineStart, lineEnd, region}: ImportCodeTokenMeta, + {filePath}: MarkdownEnv +): { + importFilePath: string | null + importCode: string +} => { + const {importFilePath, error} = resolveVersionedPath(importPath, filePath); + if (importFilePath === null || error !== null){ + return {importFilePath, importCode: error!}; + } + + // read file content + const fileContent = fs.readFileSync(importFilePath).toString(); + + const removeSpaces = (l: string[]) => { + if (l.length === 0) return l; + const spaces = l[0].length - l[0].trimStart().length; + if (spaces === 0) return l; + return l.map(v => v.substr(spaces)); + } + + const allLines = (fileContent != null) ? fileContent.split('\n') : null; + if (!allLines) return {importFilePath, importCode: "Code is empty"}; + if (region) { + const reg = findRegion(allLines, region); + if (reg) { + lineStart = reg.start + 1; + lineEnd = reg.end; + } + } + + const lines = allLines + .slice(lineStart ? lineStart - 1 : lineStart, lineEnd) + .map(x => x.replace(/\t/g, " ")); + const code = removeSpaces(lines) + .join('\n') + .replace(/\n?$/, '\n'); + + // resolve partial import + return { + importFilePath, + importCode: code, + }; +} diff --git a/docs/.vuepress/markdown/xode/types.ts b/docs/.vuepress/markdown/xode/types.ts new file mode 100644 index 000000000..f3a34d621 --- /dev/null +++ b/docs/.vuepress/markdown/xode/types.ts @@ -0,0 +1,15 @@ +export interface ImportCodeTokenMeta { + importPath: string; + lineStart: number; + lineEnd?: number; + region?: string; +} + +export interface ResolvedImport { + label?: string; + importPath: string; +} + +export interface ExtendedCodeImportPluginOptions { + handleImportPath?: (files: string) => ResolvedImport[]; +} diff --git a/docs/.vuepress/public/Kurrent Logo - Black.svg b/docs/.vuepress/public/Kurrent Logo - Black.svg new file mode 100644 index 000000000..6e2ea3bb0 --- /dev/null +++ b/docs/.vuepress/public/Kurrent Logo - Black.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/.vuepress/public/Kurrent Logo - Plum.svg b/docs/.vuepress/public/Kurrent Logo - Plum.svg new file mode 100644 index 000000000..ea6f86961 --- /dev/null +++ b/docs/.vuepress/public/Kurrent Logo - Plum.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/.vuepress/public/Kurrent Logo - White.svg b/docs/.vuepress/public/Kurrent Logo - White.svg new file mode 100644 index 000000000..739a8cea2 --- /dev/null +++ b/docs/.vuepress/public/Kurrent Logo - White.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/.vuepress/public/cloud.png b/docs/.vuepress/public/cloud.png new file mode 100644 index 0000000000000000000000000000000000000000..c9c61b8f5bf1fbcf369473db2006424ba6eae5c4 GIT binary patch literal 247451 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

Fp&B+>MSu|fWLNJGM%Ob#)m9!1fn-lf6damKEV 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

p;j-aF1zH6vsc>Zs`I9Z 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 literal 0 HcmV?d00001 diff --git a/docs/.vuepress/public/fonts/Solina-Regular.woff b/docs/.vuepress/public/fonts/Solina-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..2753ec2fd2d66b96ab13614158be584d454ca288 GIT binary patch literal 17800 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 literal 0 HcmV?d00001 diff --git a/docs/.vuepress/public/fonts/Solina-Regular.woff2 b/docs/.vuepress/public/fonts/Solina-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..29565392eedd536882ca26a9432b3bb7d3a0ccdd GIT binary patch literal 16312 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 diff --git a/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Control.cs b/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Control.cs index f2e8aa82f..9036758d5 100644 --- a/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Control.cs +++ b/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Control.cs @@ -1,5 +1,6 @@ using EventStore.Client; -using EventStore.Client.Projections; +using KurrentDB.Protocol.Projections.V1; +using static KurrentDB.Protocol.Projections.V1.Projections; namespace KurrentDB.Client { public partial class KurrentDBProjectionManagementClient { @@ -14,7 +15,7 @@ public partial class KurrentDBProjectionManagementClient { public async Task EnableAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.ProjectionsClient( + using var call = new ProjectionsClient( channelInfo.CallInvoker).EnableAsync(new EnableReq { Options = new EnableReq.Types.Options { Name = name @@ -34,7 +35,7 @@ public async Task EnableAsync(string name, TimeSpan? deadline = null, UserCreden public async Task ResetAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.ProjectionsClient( + using var call = new ProjectionsClient( channelInfo.CallInvoker).ResetAsync(new ResetReq { Options = new ResetReq.Types.Options { Name = name, @@ -78,7 +79,7 @@ public Task DisableAsync(string name, TimeSpan? deadline = null, UserCredentials public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.ProjectionsClient( + using var call = new ProjectionsClient( channelInfo.CallInvoker).RestartSubsystemAsync(new Empty(), KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); @@ -87,7 +88,7 @@ public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentia private async Task DisableInternalAsync(string name, bool writeCheckpoint, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.ProjectionsClient( + using var call = new ProjectionsClient( channelInfo.CallInvoker).DisableAsync(new DisableReq { Options = new DisableReq.Types.Options { Name = name, diff --git a/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Create.cs b/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Create.cs index 0d70d7e0e..e98794e22 100644 --- a/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Create.cs +++ b/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Create.cs @@ -1,5 +1,6 @@ using EventStore.Client; -using EventStore.Client.Projections; +using KurrentDB.Protocol.Projections.V1; +using static KurrentDB.Protocol.Projections.V1.Projections; namespace KurrentDB.Client { public partial class KurrentDBProjectionManagementClient { @@ -14,7 +15,7 @@ public partial class KurrentDBProjectionManagementClient { public async Task CreateOneTimeAsync(string query, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.ProjectionsClient( + using var call = new ProjectionsClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { OneTime = new Empty(), @@ -38,7 +39,7 @@ public async Task CreateContinuousAsync(string name, string query, bool trackEmi TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.ProjectionsClient( + using var call = new ProjectionsClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { Continuous = new CreateReq.Types.Options.Types.Continuous { @@ -63,7 +64,7 @@ public async Task CreateContinuousAsync(string name, string query, bool trackEmi public async Task CreateTransientAsync(string name, string query, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.ProjectionsClient( + using var call = new ProjectionsClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { Transient = new CreateReq.Types.Options.Types.Transient { diff --git a/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.State.cs b/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.State.cs index e5697bf19..809ac7c8c 100644 --- a/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.State.cs +++ b/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.State.cs @@ -1,6 +1,7 @@ using System.Text.Json; -using EventStore.Client.Projections; using Google.Protobuf.WellKnownTypes; +using KurrentDB.Protocol.Projections.V1; +using static KurrentDB.Protocol.Projections.V1.Projections; using Type = System.Type; namespace KurrentDB.Client { @@ -70,7 +71,7 @@ public async Task GetResultAsync(string name, string? partition = null, private async ValueTask GetResultInternalAsync(string name, string? partition, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.ProjectionsClient( + using var call = new ProjectionsClient( channelInfo.CallInvoker).ResultAsync(new ResultReq { Options = new ResultReq.Types.Options { Name = name, @@ -145,7 +146,7 @@ public async Task GetStateAsync(string name, string? partition = null, private async ValueTask GetStateInternalAsync(string name, string? partition, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.ProjectionsClient( + using var call = new ProjectionsClient( channelInfo.CallInvoker).StateAsync(new StateReq { Options = new StateReq.Types.Options { Name = name, diff --git a/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Statistics.cs b/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Statistics.cs index d61d3b9e8..e7319f082 100644 --- a/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Statistics.cs +++ b/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Statistics.cs @@ -1,7 +1,8 @@ using System.Runtime.CompilerServices; using EventStore.Client; -using EventStore.Client.Projections; using Grpc.Core; +using KurrentDB.Protocol.Projections.V1; +using static KurrentDB.Protocol.Projections.V1.Projections; namespace KurrentDB.Client { public partial class KurrentDBProjectionManagementClient { @@ -72,7 +73,7 @@ private async IAsyncEnumerable ListInternalAsync(StatisticsRe CallOptions callOptions, [EnumeratorCancellation] CancellationToken cancellationToken) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.ProjectionsClient( + using var call = new ProjectionsClient( channelInfo.CallInvoker).Statistics(new StatisticsReq { Options = options }, callOptions); diff --git a/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Update.cs b/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Update.cs index 649179ce7..a1a511f86 100644 --- a/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Update.cs +++ b/src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Update.cs @@ -1,5 +1,6 @@ using EventStore.Client; -using EventStore.Client.Projections; +using KurrentDB.Protocol.Projections.V1; +using static KurrentDB.Protocol.Projections.V1.Projections; namespace KurrentDB.Client { public partial class KurrentDBProjectionManagementClient { @@ -27,7 +28,7 @@ public async Task UpdateAsync(string name, string query, bool? emitEnabled = nul } var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Projections.ProjectionsClient( + using var call = new ProjectionsClient( channelInfo.CallInvoker).UpdateAsync(new UpdateReq { Options = options }, KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.Append.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.Append.cs index 66563d2ba..d7f813baa 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.Append.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.Append.cs @@ -1,7 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Threading.Channels; -using EventStore.Client.Streams; using Google.Protobuf; using Grpc.Core; using Microsoft.Extensions.Logging; @@ -9,6 +8,8 @@ using KurrentDB.Diagnostics.Telemetry; using KurrentDB.Diagnostics.Tracing; using KurrentDB.Client.Diagnostics; +using KurrentDB.Protocol.Streams.V1; +using static KurrentDB.Protocol.Streams.V1.Streams; namespace KurrentDB.Client { public partial class KurrentDBClient { @@ -75,7 +76,7 @@ CancellationToken cancellationToken return KurrentDBClientDiagnostics.ActivitySource.TraceClientOperation(Operation, TracingConstants.Operations.Append, tags); async ValueTask Operation() { - using var call = new Streams.StreamsClient(channelInfo.CallInvoker) + using var call = new StreamsClient(channelInfo.CallInvoker) .Append(KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.RequestStream @@ -265,7 +266,7 @@ async Task Duplex(ValueTask channelInfoTask) { return; } - _call = new Streams.StreamsClient(_channelInfo.CallInvoker).BatchAppend( + _call = new StreamsClient(_channelInfo.CallInvoker).BatchAppend( KurrentDBCallOptions.CreateStreaming( _settings, userCredentials: _settings.DefaultCredentials, diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.Delete.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.Delete.cs index 1e331a2bf..702b1b8d5 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.Delete.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.Delete.cs @@ -1,5 +1,6 @@ -using EventStore.Client.Streams; +using KurrentDB.Protocol.Streams.V1; using Microsoft.Extensions.Logging; +using static KurrentDB.Protocol.Streams.V1.Streams; namespace KurrentDB.Client { public partial class KurrentDBClient { @@ -29,7 +30,7 @@ private async Task DeleteInternal(DeleteReq request, CancellationToken cancellationToken) { _log.LogDebug("Deleting stream {streamName}.", request.Options.StreamIdentifier); var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Streams.StreamsClient( + using var call = new StreamsClient( channelInfo.CallInvoker).DeleteAsync(request, KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); var result = await call.ResponseAsync.ConfigureAwait(false); diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.Metadata.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.Metadata.cs index 629d12ced..2541257c0 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.Metadata.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.Metadata.cs @@ -1,5 +1,5 @@ using System.Text.Json; -using EventStore.Client.Streams; +using KurrentDB.Protocol.Streams.V1; using Microsoft.Extensions.Logging; namespace KurrentDB.Client { diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.Read.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.Read.cs index 2347550b4..1d1d361c6 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.Read.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.Read.cs @@ -1,7 +1,8 @@ using System.Threading.Channels; -using EventStore.Client.Streams; using Grpc.Core; -using static EventStore.Client.Streams.ReadResp.ContentOneofCase; +using KurrentDB.Protocol.Streams.V1; +using static KurrentDB.Protocol.Streams.V1.Streams; +using static KurrentDB.Protocol.Streams.V1.ReadResp.ContentOneofCase; namespace KurrentDB.Client { public partial class KurrentDBClient { @@ -164,7 +165,7 @@ CancellationToken cancellationToken async Task PumpMessages() { try { var callInvoker = await selectCallInvoker(linkedCancellationToken).ConfigureAwait(false); - var client = new Streams.StreamsClient(callInvoker); + var client = new StreamsClient(callInvoker); using var call = client.Read(request, callOptions); await foreach (var response in call.ResponseStream.ReadAllAsync(linkedCancellationToken) .ConfigureAwait(false)) { @@ -368,7 +369,7 @@ async Task PumpMessages() { try { var callInvoker = await selectCallInvoker(linkedCancellationToken).ConfigureAwait(false); - var client = new Streams.StreamsClient(callInvoker); + var client = new StreamsClient(callInvoker); using var call = client.Read(request, callOptions); await foreach (var response in call.ResponseStream.ReadAllAsync(linkedCancellationToken) diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.Subscriptions.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.Subscriptions.cs index 4824b438d..72ce47ca5 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.Subscriptions.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.Subscriptions.cs @@ -1,8 +1,9 @@ using System.Threading.Channels; using KurrentDB.Client.Diagnostics; -using EventStore.Client.Streams; using Grpc.Core; -using static EventStore.Client.Streams.ReadResp.ContentOneofCase; +using KurrentDB.Protocol.Streams.V1; +using static KurrentDB.Protocol.Streams.V1.ReadResp.ContentOneofCase; +using static KurrentDB.Protocol.Streams.V1.Streams; namespace KurrentDB.Client { public partial class KurrentDBClient { @@ -201,7 +202,7 @@ CancellationToken cancellationToken async Task PumpMessages() { try { var channelInfo = await selectChannelInfo(_cts.Token).ConfigureAwait(false); - var client = new Streams.StreamsClient(channelInfo.CallInvoker); + var client = new StreamsClient(channelInfo.CallInvoker); _call = client.Read(_request, _callOptions); await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { StreamMessage subscriptionMessage = diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.Tombstone.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.Tombstone.cs index 4fd3d2db2..bd1c7bdf7 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.Tombstone.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.Tombstone.cs @@ -1,5 +1,6 @@ -using EventStore.Client.Streams; +using KurrentDB.Protocol.Streams.V1; using Microsoft.Extensions.Logging; +using static KurrentDB.Protocol.Streams.V1.Streams; namespace KurrentDB.Client { public partial class KurrentDBClient { @@ -28,7 +29,7 @@ private async Task TombstoneInternal(TombstoneReq request, TimeSpa _log.LogDebug("Tombstoning stream {streamName}.", request.Options.StreamIdentifier); var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Streams.StreamsClient( + using var call = new StreamsClient( channelInfo.CallInvoker).TombstoneAsync(request, KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); var result = await call.ResponseAsync.ConfigureAwait(false); diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.cs index 9163cf1f8..a4cdfa947 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.cs @@ -2,10 +2,10 @@ using System.Threading.Channels; using EventStore.Client; using Grpc.Core; +using KurrentDB.Protocol.Streams.V1; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using ReadReq = EventStore.Client.Streams.ReadReq; namespace KurrentDB.Client { /// diff --git a/src/KurrentDB.Client/Streams/Streams/AppendReq.cs b/src/KurrentDB.Client/Streams/Streams/AppendReq.cs index 32483d9d2..9a866b580 100644 --- a/src/KurrentDB.Client/Streams/Streams/AppendReq.cs +++ b/src/KurrentDB.Client/Streams/Streams/AppendReq.cs @@ -1,19 +1,20 @@ +using EventStore.Client; using KurrentDB.Client; -namespace EventStore.Client.Streams { - partial class AppendReq { - public AppendReq WithAnyStreamRevision(StreamState expectedState) { - if (expectedState == StreamState.Any) { - Options.Any = new Empty(); - } else if (expectedState == StreamState.NoStream) { - Options.NoStream = new Empty(); - } else if (expectedState == StreamState.StreamExists) { - Options.StreamExists = new Empty(); - } else { - Options.Revision = (ulong)expectedState.ToInt64(); - } +namespace KurrentDB.Protocol.Streams.V1; - return this; +partial class AppendReq { + public AppendReq WithAnyStreamRevision(StreamState expectedState) { + if (expectedState == StreamState.Any) { + Options.Any = new Empty(); + } else if (expectedState == StreamState.NoStream) { + Options.NoStream = new Empty(); + } else if (expectedState == StreamState.StreamExists) { + Options.StreamExists = new Empty(); + } else { + Options.Revision = (ulong)expectedState.ToInt64(); } + + return this; } } diff --git a/src/KurrentDB.Client/Streams/Streams/BatchAppendReq.cs b/src/KurrentDB.Client/Streams/Streams/BatchAppendReq.cs index 1eb679fb1..85233e4fa 100644 --- a/src/KurrentDB.Client/Streams/Streams/BatchAppendReq.cs +++ b/src/KurrentDB.Client/Streams/Streams/BatchAppendReq.cs @@ -1,7 +1,8 @@ +using EventStore.Client; using Google.Protobuf.WellKnownTypes; using KurrentDB.Client; -namespace EventStore.Client.Streams { +namespace KurrentDB.Protocol.Streams.V1 { partial class BatchAppendReq { partial class Types { partial class Options { diff --git a/src/KurrentDB.Client/Streams/Streams/BatchAppendResp.cs b/src/KurrentDB.Client/Streams/Streams/BatchAppendResp.cs index 38da31f17..11120e62e 100644 --- a/src/KurrentDB.Client/Streams/Streams/BatchAppendResp.cs +++ b/src/KurrentDB.Client/Streams/Streams/BatchAppendResp.cs @@ -1,9 +1,12 @@ +using EventStore.Client; using Grpc.Core; using KurrentDB.Client; using static EventStore.Client.WrongExpectedVersion.CurrentStreamRevisionOptionOneofCase; using static EventStore.Client.WrongExpectedVersion.ExpectedStreamPositionOptionOneofCase; +using Position = KurrentDB.Client.Position; +using Timeout = EventStore.Client.Timeout; -namespace EventStore.Client.Streams { +namespace KurrentDB.Protocol.Streams.V1 { partial class BatchAppendResp { public IWriteResult ToWriteResult() => ResultCase switch { ResultOneofCase.Success => new SuccessResult( diff --git a/src/KurrentDB.Client/Streams/Streams/DeleteReq.cs b/src/KurrentDB.Client/Streams/Streams/DeleteReq.cs index 28651ca47..579cb6205 100644 --- a/src/KurrentDB.Client/Streams/Streams/DeleteReq.cs +++ b/src/KurrentDB.Client/Streams/Streams/DeleteReq.cs @@ -1,6 +1,7 @@ +using EventStore.Client; using KurrentDB.Client; -namespace EventStore.Client.Streams { +namespace KurrentDB.Protocol.Streams.V1 { partial class DeleteReq { public DeleteReq WithAnyStreamRevision(StreamState expectedState) { if (expectedState == StreamState.Any) { diff --git a/src/KurrentDB.Client/Streams/Streams/ReadReq.cs b/src/KurrentDB.Client/Streams/Streams/ReadReq.cs index 1a8b513e4..b27130b00 100644 --- a/src/KurrentDB.Client/Streams/Streams/ReadReq.cs +++ b/src/KurrentDB.Client/Streams/Streams/ReadReq.cs @@ -1,7 +1,7 @@ -using System; +using EventStore.Client; using KurrentDB.Client; -namespace EventStore.Client.Streams { +namespace KurrentDB.Protocol.Streams.V1 { partial class ReadReq { partial class Types { partial class Options { diff --git a/src/KurrentDB.Client/Streams/Streams/TombstoneReq.cs b/src/KurrentDB.Client/Streams/Streams/TombstoneReq.cs index b86eb26d6..5273c38b4 100644 --- a/src/KurrentDB.Client/Streams/Streams/TombstoneReq.cs +++ b/src/KurrentDB.Client/Streams/Streams/TombstoneReq.cs @@ -1,6 +1,7 @@ +using EventStore.Client; using KurrentDB.Client; -namespace EventStore.Client.Streams { +namespace KurrentDB.Protocol.Streams.V1 { partial class TombstoneReq { public TombstoneReq WithAnyStreamRevision(StreamState expectedState) { if (expectedState == StreamState.Any) { diff --git a/src/KurrentDB.Client/UserManagement/KurrentDBUserManagementClient.cs b/src/KurrentDB.Client/UserManagement/KurrentDBUserManagementClient.cs index a7f801c99..d8a48dd06 100644 --- a/src/KurrentDB.Client/UserManagement/KurrentDBUserManagementClient.cs +++ b/src/KurrentDB.Client/UserManagement/KurrentDBUserManagementClient.cs @@ -1,8 +1,9 @@ using System.Runtime.CompilerServices; -using EventStore.Client.Users; using Grpc.Core; +using KurrentDB.Protocol.Users.V1; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using static KurrentDB.Protocol.Users.V1.Users; namespace KurrentDB.Client { /// @@ -46,7 +47,7 @@ public async Task CreateUserAsync(string loginName, string fullName, string[] gr if (password == string.Empty) throw new ArgumentOutOfRangeException(nameof(password)); var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Users.UsersClient( + using var call = new UsersClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { LoginName = loginName, @@ -79,7 +80,7 @@ public async Task GetUserAsync(string loginName, TimeSpan? deadline } var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Users.UsersClient( + using var call = new UsersClient( channelInfo.CallInvoker).Details(new DetailsReq { Options = new DetailsReq.Types.Options { LoginName = loginName @@ -116,7 +117,7 @@ public async Task DeleteUserAsync(string loginName, TimeSpan? deadline = null, } var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - var call = new Users.UsersClient( + var call = new UsersClient( channelInfo.CallInvoker).DeleteAsync(new DeleteReq { Options = new DeleteReq.Types.Options { LoginName = loginName @@ -146,7 +147,7 @@ public async Task EnableUserAsync(string loginName, TimeSpan? deadline = null, } var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Users.UsersClient( + using var call = new UsersClient( channelInfo.CallInvoker).EnableAsync(new EnableReq { Options = new EnableReq.Types.Options { LoginName = loginName @@ -169,7 +170,7 @@ public async Task DisableUserAsync(string loginName, TimeSpan? deadline = null, if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - var call = new Users.UsersClient( + var call = new UsersClient( channelInfo.CallInvoker).DisableAsync(new DisableReq { Options = new DisableReq.Types.Options { LoginName = loginName @@ -189,7 +190,7 @@ public async IAsyncEnumerable ListAllAsync(TimeSpan? deadline = nul UserCredentials? userCredentials = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Users.UsersClient( + using var call = new UsersClient( channelInfo.CallInvoker).Details(new DetailsReq(), KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); @@ -225,7 +226,7 @@ public async Task ChangePasswordAsync(string loginName, string currentPassword, if (newPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(newPassword)); var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Users.UsersClient( + using var call = new UsersClient( channelInfo.CallInvoker).ChangePasswordAsync( new ChangePasswordReq { Options = new ChangePasswordReq.Types.Options { diff --git a/test/KurrentDB.Client.Tests.Common/Fixtures/BaseTestNode.cs b/test/KurrentDB.Client.Tests.Common/Fixtures/BaseTestNode.cs deleted file mode 100644 index df23f8bfa..000000000 --- a/test/KurrentDB.Client.Tests.Common/Fixtures/BaseTestNode.cs +++ /dev/null @@ -1,162 +0,0 @@ -// // ReSharper disable InconsistentNaming -// -// using System.Globalization; -// using System.Net; -// using System.Net.Sockets; -// using Ductus.FluentDocker.Builders; -// using Ductus.FluentDocker.Extensions; -// using Ductus.FluentDocker.Services.Extensions; -// using KurrentDB.Client.Tests.FluentDocker; -// using Humanizer; -// using Serilog; -// using Serilog.Extensions.Logging; -// using static System.TimeSpan; -// -// namespace KurrentDB.Client.Tests; -// -// public abstract class BaseTestNode(EventStoreFixtureOptions? options = null) : TestContainerService { -// static readonly NetworkPortProvider NetworkPortProvider = new(NetworkPortProvider.DefaultEsdbPort); -// -// public KurrentDBFixtureOptions Options { get; } = options ?? DefaultOptions(); -// -// static Version? _version; -// -// public static Version Version => _version ??= GetVersion(); -// -// public static EventStoreFixtureOptions DefaultOptions() { -// const string connString = "esdb://admin:changeit@localhost:{port}/?tlsVerifyCert=false"; -// -// var port = NetworkPortProvider.NextAvailablePort; -// -// var defaultSettings = EventStoreClientSettings -// .Create(connString.Replace("{port}", $"{port}")) -// .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)) -// .With(x => x.DefaultDeadline = Application.DebuggerIsAttached ? new TimeSpan?() : FromSeconds(30)) -// .With(x => x.ConnectivitySettings.MaxDiscoverAttempts = 20) -// .With(x => x.ConnectivitySettings.DiscoveryInterval = FromSeconds(1)); -// -// var defaultEnvironment = new Dictionary(GlobalEnvironment.Variables) { -// // ["EVENTSTORE_MEM_DB"] = "true", -// // ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", -// // ["EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE"] = "/etc/eventstore/certs/node/node.key", -// // ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", -// // ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", -// // ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "true", -// // ["EVENTSTORE_LOG_LEVEL"] = "Default", // required to use serilog settings -// // ["EVENTSTORE_DISABLE_LOG_FILE"] = "true", -// // ["EVENTSTORE_START_STANDARD_PROJECTIONS"] = "true", -// // ["EVENTSTORE_RUN_PROJECTIONS"] = "All", -// // ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), -// // ["EVENTSTORE_MAX_APPEND_SIZE"] = 100.Kilobytes().Bytes.ToString(CultureInfo.InvariantCulture), -// // ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" -// -// ["EVENTSTORE_MEM_DB"] = "true", -// ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", -// ["EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE"] = "/etc/eventstore/certs/node/node.key", -// ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", -// ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", -// ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "true", -// ["EVENTSTORE_LOG_LEVEL"] = "Default", // required to use serilog settings -// ["EVENTSTORE_DISABLE_LOG_FILE"] = "true", -// ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), -// ["EVENTSTORE_MAX_APPEND_SIZE"] = 100.Kilobytes().Bytes.ToString(CultureInfo.InvariantCulture), -// ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" -// }; -// -// if (port != NetworkPortProvider.DefaultEsdbPort) { -// if (GlobalEnvironment.Variables.TryGetValue("ES_DOCKER_TAG", out var tag) && tag == "ci") -// defaultEnvironment["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{port}"; -// else -// defaultEnvironment["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{port}"; -// } -// -// return new(defaultSettings, defaultEnvironment); -// } -// -// static Version GetVersion() { -// const string versionPrefix = "EventStoreDB version"; -// -// using var cts = new CancellationTokenSource(FromSeconds(30)); -// using var eventstore = new Builder().UseContainer() -// .UseImage(GlobalEnvironment.DockerImage) -// .Command("--version") -// .Build() -// .Start(); -// -// using var log = eventstore.Logs(true, cts.Token); -// foreach (var line in log.ReadToEnd()) { -// if (line.StartsWith(versionPrefix) && -// Version.TryParse(new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), out var version)) { -// return version; -// } -// } -// -// throw new InvalidOperationException("Could not determine server version."); -// -// IEnumerable ReadVersion(string s) { -// foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { -// yield return c; -// } -// } -// } -// -// string[] GetEnvironmentVariables() => -// Options.Environment.Select(pair => $"{pair.Key}={pair.Value}").ToArray(); -// -// protected abstract ContainerBuilder ConfigureContainer(ContainerBuilder builder); -// -// protected override ContainerBuilder Configure() { -// var certsPath = Path.Combine(Environment.CurrentDirectory, "certs"); -// -// CertificatesManager.VerifyCertificatesExist(certsPath); -// -// var builder = new Builder().UseContainer().WithEnvironment(GetEnvironmentVariables()); -// -// return ConfigureContainer(builder); -// } -// } -// -// /// -// /// Using the default 2113 port assumes that the test is running sequentially. -// /// -// /// -// class NetworkPortProvider(int port = 2114) { -// public const int DefaultEsdbPort = 2113; -// -// static readonly SemaphoreSlim Semaphore = new(1, 1); -// -// async Task GetNextAvailablePort(TimeSpan delay = default) { -// if (port == DefaultEsdbPort) -// return port; -// -// await Semaphore.WaitAsync(); -// -// try { -// using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); -// -// while (true) { -// var nexPort = Interlocked.Increment(ref port); -// -// try { -// await socket.ConnectAsync(IPAddress.Any, nexPort); -// } catch (SocketException ex) { -// if (ex.SocketErrorCode is SocketError.ConnectionRefused or not SocketError.IsConnected) { -// return nexPort; -// } -// -// await Task.Delay(delay); -// } finally { -// #if NET -// if (socket.Connected) await socket.DisconnectAsync(true); -// #else -// if (socket.Connected) socket.Disconnect(true); -// #endif -// } -// } -// } finally { -// Semaphore.Release(); -// } -// } -// -// public int NextAvailablePort => GetNextAvailablePort(FromMilliseconds(100)).GetAwaiter().GetResult(); -// } diff --git a/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBPermanentFixture.cs b/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBPermanentFixture.cs index 7dced2d4e..a1bb17b55 100644 --- a/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBPermanentFixture.cs +++ b/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBPermanentFixture.cs @@ -1,10 +1,6 @@ // ReSharper disable InconsistentNaming -using System.Net; -using Ductus.FluentDocker.Builders; -using Ductus.FluentDocker.Extensions; -using Ductus.FluentDocker.Services.Extensions; -using KurrentDB.Client; +using System.Net.Http; using KurrentDB.Client.Tests.FluentDocker; using Serilog; using static System.TimeSpan; @@ -13,18 +9,15 @@ namespace KurrentDB.Client.Tests; [PublicAPI] public partial class KurrentDBPermanentFixture : IAsyncLifetime, IAsyncDisposable { - static readonly ILogger Logger; + static readonly ILogger Logger; + static readonly SemaphoreSlim WarmUpGatekeeper = new(1, 1); static KurrentDBPermanentFixture() { Logging.Initialize(); Logger = Serilog.Log.ForContext(); -#if NET9_0_OR_GREATER var httpClientHandler = new HttpClientHandler(); httpClientHandler.ServerCertificateCustomValidationCallback = delegate { return true; }; -#else - ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; -#endif } public KurrentDBPermanentFixture() : this(options => options) { } @@ -38,21 +31,18 @@ protected KurrentDBPermanentFixture(ConfigureFixture configure) { public ILogger Log => Logger; - public ITestService Service { get; } - public KurrentDBFixtureOptions Options { get; } - public Faker Faker { get; } = new Faker(); - - public Version EventStoreVersion { get; private set; } = null!; - - public bool IsKdb => EventStoreVersion.Major >= 25; + public KurrentDBPermanentTestNode Service { get; } + public KurrentDBFixtureOptions Options { get; } + public Faker Faker { get; } = new(); - public bool EventStoreHasLastStreamPosition { get; private set; } + public Version DatabaseVersion { get; private set; } = null!; + public bool HasLastStreamPosition { get; private set; } public KurrentDBClient Streams { get; private set; } = null!; - public KurrentDBUserManagementClient DBUsers { get; private set; } = null!; - public KurrentDBProjectionManagementClient DBProjections { get; private set; } = null!; + public KurrentDBUserManagementClient DBUsers { get; private set; } = null!; + public KurrentDBProjectionManagementClient DBProjections { get; private set; } = null!; public KurrentDBPersistentSubscriptionsClient Subscriptions { get; private set; } = null!; - public KurrentDBOperationsClient DBOperations { get; private set; } = null!; + public KurrentDBOperationsClient DBOperations { get; private set; } = null!; public bool SkipPsWarmUp { get; set; } @@ -75,36 +65,30 @@ protected KurrentDBPermanentFixture(ConfigureFixture configure) { DefaultDeadline = Options.DBClientSettings.DefaultDeadline }; - InterlockedBoolean WarmUpCompleted { get; } = new InterlockedBoolean(); - static readonly SemaphoreSlim WarmUpGatekeeper = new(1, 1); + InterlockedBoolean WarmUpCompleted { get; } = new(); - public void CaptureTestRun(ITestOutputHelper outputHelper) { - var testRunId = Logging.CaptureLogs(outputHelper); - TestRuns.Add(testRunId); - Logger.Information(">>> Test Run {TestRunId} {Operation} <<<", testRunId, "starting"); - Service.ReportStatus(); - } + async ValueTask IAsyncDisposable.DisposeAsync() => await DisposeAsync(); public async Task InitializeAsync() { await WarmUpGatekeeper.WaitAsync(); try { await Service.Start(); - EventStoreVersion = GetKurrentVersion(); - EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; + DatabaseVersion = TestContainerService.Version; + HasLastStreamPosition = (DatabaseVersion?.Major ?? int.MaxValue) >= 21; if (!WarmUpCompleted.CurrentValue) { Logger.Warning("*** Warmup started ***"); await Task.WhenAll( InitClient(async x => DBUsers = await Task.FromResult(x)), - InitClient(async x => Streams = await Task.FromResult(x)), + InitClient(async x => Streams = await Task.FromResult(x)), InitClient( async x => DBProjections = await Task.FromResult(x), Options.Environment["EVENTSTORE_RUN_PROJECTIONS"] != "None" ), InitClient(async x => Subscriptions = SkipPsWarmUp ? x : await Task.FromResult(x)), - InitClient(async x => DBOperations = await Task.FromResult(x)) + InitClient(async x => DBOperations = await Task.FromResult(x)) ); WarmUpCompleted.EnsureCalledOnce(); @@ -128,42 +112,6 @@ async Task InitClient(Func action, bool execute = true) where T : await action(client); return client; } - - static Version GetKurrentVersion() { - const string versionPrefix = "KurrentDB version"; - const string esdbVersionPrefix = "EventStoreDB version"; - - using var cancellator = new CancellationTokenSource(FromSeconds(30)); - using var eventstore = new Builder() - .UseContainer() - .UseImage(GlobalEnvironment.DockerImage) - .Command("--version") - .Build() - .Start(); - - using var log = eventstore.Logs(true, cancellator.Token); - var logs = log.ReadToEnd(); - foreach (var line in logs) { - Logger.Information("KurrentDB: {Line}", line); - if (line.StartsWith(versionPrefix) && - Version.TryParse(new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), out var version)) { - return version; - } - - if (line.StartsWith(esdbVersionPrefix) && - Version.TryParse(new string(ReadVersion(line[(esdbVersionPrefix.Length + 1)..]).ToArray()), out var esdbVersion)) { - return esdbVersion; - } - } - - throw new InvalidOperationException($"Could not determine server version from logs: {string.Join(Environment.NewLine, logs)}"); - - IEnumerable ReadVersion(string s) { - foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { - yield return c; - } - } - } } public async Task DisposeAsync() { @@ -179,7 +127,12 @@ public async Task DisposeAsync() { Logging.ReleaseLogs(testRunId); } - async ValueTask IAsyncDisposable.DisposeAsync() => await DisposeAsync(); + public void CaptureTestRun(ITestOutputHelper outputHelper) { + var testRunId = Logging.CaptureLogs(outputHelper); + TestRuns.Add(testRunId); + Logger.Information(">>> Test Run {TestRunId} {Operation} <<<", testRunId, "starting"); + Service.ReportStatus(); + } } public abstract class KurrentDBPermanentTests : IClassFixture where TFixture : KurrentDBPermanentFixture { diff --git a/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBPermanentTestNode.cs b/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBPermanentTestNode.cs index 34f2db509..52b4d577c 100644 --- a/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBPermanentTestNode.cs +++ b/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBPermanentTestNode.cs @@ -1,213 +1,41 @@ // ReSharper disable InconsistentNaming +// ReSharper disable InvertIf -// using Ductus.FluentDocker.Builders; -// using Ductus.FluentDocker.Model.Builders; -// using KurrentDB.Client.Tests.FluentDocker; -// -// namespace KurrentDB.Client.Tests; -// -// public class EventStorePermanentTestNode(EventStoreFixtureOptions? options = null) : BaseTestNode(options) { -// protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder) { -// var port = Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; -// var certsPath = Path.Combine(Environment.CurrentDirectory, "certs"); -// -// var containerName = "es-client-dotnet-test"; -// -// return builder -// .UseImage(Options.Environment["ES_DOCKER_IMAGE"]) -// .WithName(containerName) -// .WithPublicEndpointResolver() -// .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) -// .ExposePort(port, 2113) -// .KeepContainer().KeepRunning().ReuseIfExists() -// .WaitUntilReadyWithConstantBackoff( -// 1_000, -// 60, -// service => { -// var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); -// if (!output.Success) -// throw new Exception(output.Error); -// } -// ); -// } -// } - -using System.Globalization; -using System.Net; -using System.Net.Sockets; using Ductus.FluentDocker.Builders; -using Ductus.FluentDocker.Extensions; -using Ductus.FluentDocker.Model.Builders; -using Ductus.FluentDocker.Services.Extensions; -using KurrentDB.Client; using KurrentDB.Client.Tests.FluentDocker; -using Humanizer; -using KurrentDB.Client.Tests; using Serilog; using Serilog.Extensions.Logging; -using static System.TimeSpan; -public class KurrentDBPermanentTestNode(KurrentDBFixtureOptions? options = null) : TestContainerService { - static readonly NetworkPortProvider NetworkPortProvider = new(NetworkPortProvider.DefaultEsdbPort); +namespace KurrentDB.Client.Tests; +public class KurrentDBPermanentTestNode(KurrentDBFixtureOptions? options = null) : TestContainerService { KurrentDBFixtureOptions Options { get; } = options ?? DefaultOptions(); - static Version? _version; - - public static Version Version => _version ??= GetVersion(); - public static KurrentDBFixtureOptions DefaultOptions() { - const string connString = "esdb://admin:changeit@localhost:{port}/?tlsVerifyCert=false"; - - var port = NetworkPortProvider.NextAvailablePort; + const int port = 2113; var defaultSettings = KurrentDBClientSettings - .Create(connString.Replace("{port}", $"{port}")) - .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)) - .With(x => x.DefaultDeadline = Application.DebuggerIsAttached ? new TimeSpan?() : FromSeconds(30)) - .With(x => x.ConnectivitySettings.MaxDiscoverAttempts = 20) - .With(x => x.ConnectivitySettings.DiscoveryInterval = FromSeconds(1)); + .Create(ConnectionString.Replace("{port}", port.ToString())) + .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)); var defaultEnvironment = new Dictionary(GlobalEnvironment.Variables) { - ["EVENTSTORE_MEM_DB"] = "true", - ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", - ["EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE"] = "/etc/eventstore/certs/node/node.key", - ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", - ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", - ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "true", - ["EVENTSTORE_LOG_LEVEL"] = "Default", // required to use serilog settings - ["EVENTSTORE_DISABLE_LOG_FILE"] = "true", ["EVENTSTORE_START_STANDARD_PROJECTIONS"] = "true", ["EVENTSTORE_RUN_PROJECTIONS"] = "All", - ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), - ["EVENTSTORE_MAX_APPEND_SIZE"] = 100.Kilobytes().Bytes.ToString(CultureInfo.InvariantCulture), - ["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" + ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = port.ToString(), + ["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = port.ToString(), + ["EVENTSTORE_NODE_PORT"] = port.ToString(), }; - if (GlobalEnvironment.DockerImage.Contains("commercial")) { - defaultEnvironment["EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH"] = "/etc/eventstore/certs/ca"; - defaultEnvironment["EventStore__Plugins__UserCertificates__Enabled"] = "true"; - } - - if (port != NetworkPortProvider.DefaultEsdbPort) { - if (GlobalEnvironment.Variables.TryGetValue("ES_DOCKER_TAG", out var tag) && tag == "ci") - defaultEnvironment["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{port}"; - else - defaultEnvironment["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{port}"; - } - return new(defaultSettings, defaultEnvironment); } - static Version GetVersion() { - const string versionPrefix = "KurrentDB version"; - const string esdbVersionPrefix = "EventStoreDB version"; - - using var cts = new CancellationTokenSource(FromSeconds(30)); - using var eventstore = new Builder().UseContainer() - .UseImage(GlobalEnvironment.DockerImage) - .Command("--version") - .Build() - .Start(); - - using var log = eventstore.Logs(true, cts.Token); - var logs = log.ReadToEnd(); - foreach (var line in logs) { - if (line.StartsWith(versionPrefix) && - Version.TryParse(new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), out var version)) { - return version; - } - - if (line.StartsWith(esdbVersionPrefix) && - Version.TryParse(new string(ReadVersion(line[(esdbVersionPrefix.Length + 1)..]).ToArray()), out var esdbVersion)) { - return esdbVersion; - } - } - - throw new InvalidOperationException($"Could not determine server version from logs: {string.Join(Environment.NewLine, logs)}"); - - IEnumerable ReadVersion(string s) { - foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { - yield return c; - } - } - } - protected override ContainerBuilder Configure() { - var env = Options.Environment.Select(pair => $"{pair.Key}={pair.Value}").ToArray(); + var builder = CreateContainer(Options.Environment); - var port = Options.DBClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; - var certsPath = Path.Combine(Environment.CurrentDirectory, "certs"); + builder.KeepContainer() + .KeepRunning() + .ReuseIfExists(); - var containerName = port == 2113 - ? "es-client-dotnet-test" - : $"es-client-dotnet-test-{port}-{Guid.NewGuid().ToString()[30..]}"; - - CertificatesManager.VerifyCertificatesExist(certsPath); - - return new Builder() - .UseContainer() - .UseImage(Options.Environment["ES_DOCKER_IMAGE"]) - .WithName(containerName) - .WithPublicEndpointResolver() - .WithEnvironment(env) - .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) - .ExposePort(port, 2113) - .KeepContainer().KeepRunning().ReuseIfExists() - .WaitUntilReadyWithConstantBackoff( - 1_000, - 60, - service => { - var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); - if (!output.Success) - throw new Exception(output.Error); - } - ); + return AddReadinessCheck(builder); } } - -/// -/// Using the default 2113 port assumes that the test is running sequentially. -/// -/// -class NetworkPortProvider(int port = 2114) { - public const int DefaultEsdbPort = 2113; - - static readonly SemaphoreSlim Semaphore = new(1, 1); - - async Task GetNextAvailablePort(TimeSpan delay = default) { - // TODO SS: find a way to enable parallel tests on CI - if (port == DefaultEsdbPort) - return port; - - await Semaphore.WaitAsync(); - - try { - using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - - while (true) { - var nexPort = Interlocked.Increment(ref port); - - try { - await socket.ConnectAsync(IPAddress.Any, nexPort); - } catch (SocketException ex) { - if (ex.SocketErrorCode is SocketError.ConnectionRefused or not SocketError.IsConnected) { - return nexPort; - } - - await Task.Delay(delay); - } finally { -#if NET - if (socket.Connected) await socket.DisconnectAsync(true); -#else - if (socket.Connected) socket.Disconnect(true); -#endif - } - } - } finally { - Semaphore.Release(); - } - } - - public int NextAvailablePort => GetNextAvailablePort(FromMilliseconds(100)).GetAwaiter().GetResult(); -} diff --git a/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBTemporaryFixture.cs b/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBTemporaryFixture.cs index fc36b2f9e..76b57ffee 100644 --- a/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBTemporaryFixture.cs +++ b/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBTemporaryFixture.cs @@ -1,11 +1,9 @@ // ReSharper disable InconsistentNaming -using System.Net; -using Ductus.FluentDocker.Builders; -using Ductus.FluentDocker.Extensions; -using Ductus.FluentDocker.Services.Extensions; +using System.Net.Http; using KurrentDB.Client.Tests.FluentDocker; using Serilog; +using Serilog.Extensions.Logging; using static System.TimeSpan; namespace KurrentDB.Client.Tests.TestNode; @@ -18,38 +16,35 @@ static KurrentDBTemporaryFixture() { Logging.Initialize(); Logger = Serilog.Log.ForContext(); -#if NET9_0_OR_GREATER var httpClientHandler = new HttpClientHandler(); httpClientHandler.ServerCertificateCustomValidationCallback = delegate { return true; }; -#else - ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; -#endif } public KurrentDBTemporaryFixture() : this(options => options) { } protected KurrentDBTemporaryFixture(ConfigureFixture configure) { - // Options = configure(EventStoreTemporaryTestNode.DefaultOptions()); Options = configure(KurrentDBTemporaryTestNode.DefaultOptions()); Service = new KurrentDBTemporaryTestNode(Options); + + Options.DBClientSettings.LoggerFactory = new SerilogLoggerFactory(Logger); } List TestRuns { get; } = new(); public ILogger Log => Logger; - public ITestService Service { get; } + public ITestService Service { get; } public KurrentDBFixtureOptions Options { get; } - public Faker Faker { get; } = new Faker(); + public Faker Faker { get; } = new Faker(); - public Version EventStoreVersion { get; private set; } = null!; - public bool EventStoreHasLastStreamPosition { get; private set; } + public Version DatabaseVersion { get; private set; } = null!; + public bool HasLastStreamPosition { get; private set; } public KurrentDBClient Streams { get; private set; } = null!; - public KurrentDBUserManagementClient DBUsers { get; private set; } = null!; - public KurrentDBProjectionManagementClient DBProjections { get; private set; } = null!; + public KurrentDBUserManagementClient DBUsers { get; private set; } = null!; + public KurrentDBProjectionManagementClient DBProjections { get; private set; } = null!; public KurrentDBPersistentSubscriptionsClient Subscriptions { get; private set; } = null!; - public KurrentDBOperationsClient DBOperations { get; private set; } = null!; + public KurrentDBOperationsClient DBOperations { get; private set; } = null!; public bool SkipPsWarmUp { get; set; } @@ -87,8 +82,8 @@ public async Task InitializeAsync() { try { await Service.Start(); - EventStoreVersion = GetKurrentVersion(); - EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; + DatabaseVersion = TestContainerService.Version; + HasLastStreamPosition = (DatabaseVersion?.Major ?? int.MaxValue) >= 21; if (!WarmUpCompleted.CurrentValue) { Logger.Warning("*** Warmup started ***"); @@ -125,42 +120,6 @@ async Task InitClient(Func action, bool execute = true) where T : await action(client); return client; } - - static Version GetKurrentVersion() { - const string versionPrefix = "KurrentDB version"; - const string esdbVersionPrefix = "EventStoreDB version"; - - using var cancellator = new CancellationTokenSource(FromSeconds(30)); - using var eventstore = new Builder() - .UseContainer() - .UseImage(GlobalEnvironment.DockerImage) - .Command("--version") - .Build() - .Start(); - - using var log = eventstore.Logs(true, cancellator.Token); - var logs = log.ReadToEnd(); - foreach (var line in logs) { - Logger.Information("KurrentDB: {Line}", line); - if (line.StartsWith(versionPrefix) && - Version.TryParse(new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), out var version)) { - return version; - } - - if (line.StartsWith(esdbVersionPrefix) && - Version.TryParse(new string(ReadVersion(line[(esdbVersionPrefix.Length + 1)..]).ToArray()), out var esdbVersion)) { - return esdbVersion; - } - } - - throw new InvalidOperationException($"Could not determine server version from logs: {string.Join(Environment.NewLine, logs)}"); - - IEnumerable ReadVersion(string s) { - foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { - yield return c; - } - } - } } public async Task DisposeAsync() { diff --git a/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBTemporaryTestNode.cs b/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBTemporaryTestNode.cs index 9416b0705..1d9eee2c6 100644 --- a/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBTemporaryTestNode.cs +++ b/test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBTemporaryTestNode.cs @@ -1,165 +1,44 @@ // ReSharper disable InconsistentNaming +// ReSharper disable InvertIf -// using Ductus.FluentDocker.Builders; -// using Ductus.FluentDocker.Model.Builders; -// using KurrentDB.Client.Tests.FluentDocker; -// -// namespace KurrentDB.Client.Tests.TestNode; -// -// public class EventStoreTemporaryTestNode(EventStoreFixtureOptions? options = null) : BaseTestNode(options) { -// protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder) { -// var port = Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; -// var certsPath = Path.Combine(Environment.CurrentDirectory, "certs"); -// -// var containerName = $"es-client-dotnet-test-{port}-{Guid.NewGuid().ToString()[30..]}"; -// -// return builder -// .UseImage(Options.Environment["ES_DOCKER_IMAGE"]) -// .WithName(containerName) -// .WithPublicEndpointResolver() -// .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) -// .ExposePort(port, 2113) -// .WaitUntilReadyWithConstantBackoff( -// 1_000, -// 60, -// service => { -// var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); -// if (!output.Success) -// throw new Exception(output.Error); -// } -// ); -// } -// } - -using System.Globalization; using System.Net; using System.Net.Sockets; using Ductus.FluentDocker.Builders; -using Ductus.FluentDocker.Extensions; -using Ductus.FluentDocker.Model.Builders; -using Ductus.FluentDocker.Services.Extensions; -using KurrentDB.Client; -using KurrentDB.Client.Tests.FluentDocker; using Humanizer; +using KurrentDB.Client.Tests.FluentDocker; using Serilog; using Serilog.Extensions.Logging; -using static System.TimeSpan; -namespace KurrentDB.Client.Tests.TestNode; +namespace KurrentDB.Client.Tests; public class KurrentDBTemporaryTestNode(KurrentDBFixtureOptions? options = null) : TestContainerService { - static readonly NetworkPortProvider NetworkPortProvider = new(NetworkPortProvider.DefaultEsdbPort); - KurrentDBFixtureOptions Options { get; } = options ?? DefaultOptions(); - static Version? _version; - - public static Version Version => _version ??= GetVersion(); + static readonly NetworkPortProvider PortProvider = new(NetworkPortProvider.DefaultPort); public static KurrentDBFixtureOptions DefaultOptions() { - const string connString = "esdb://admin:changeit@localhost:{port}/?tlsVerifyCert=false"; - - var port = NetworkPortProvider.NextAvailablePort; + var port = PortProvider.NextAvailablePort; var defaultSettings = KurrentDBClientSettings - .Create(connString.Replace("{port}", $"{port}")) - .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)) - .With(x => x.DefaultDeadline = Application.DebuggerIsAttached ? new TimeSpan?() : FromSeconds(30)) - .With(x => x.ConnectivitySettings.MaxDiscoverAttempts = 20) - .With(x => x.ConnectivitySettings.DiscoveryInterval = FromSeconds(1)); + .Create(ConnectionString.Replace("{port}", $"{port}")) + .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)); var defaultEnvironment = new Dictionary(GlobalEnvironment.Variables) { - ["EVENTSTORE_MEM_DB"] = "true", - ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", - ["EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE"] = "/etc/eventstore/certs/node/node.key", - ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", - ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", - ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "true", - ["EVENTSTORE_LOG_LEVEL"] = "Default", // required to use serilog settings - ["EVENTSTORE_DISABLE_LOG_FILE"] = "true", - ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), - ["EVENTSTORE_MAX_APPEND_SIZE"] = 100.Kilobytes().Bytes.ToString(CultureInfo.InvariantCulture), - ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" + ["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{port}", + ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{port}", }; - if (GlobalEnvironment.DockerImage.Contains("commercial")) { - defaultEnvironment["EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH"] = "/etc/eventstore/certs/ca"; - defaultEnvironment["EventStore__Plugins__UserCertificates__Enabled"] = "true"; - } - - if (port != NetworkPortProvider.DefaultEsdbPort) { - if (GlobalEnvironment.Variables.TryGetValue("ES_DOCKER_TAG", out var tag) && tag == "ci") - defaultEnvironment["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{port}"; - else - defaultEnvironment["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{port}"; - } - return new(defaultSettings, defaultEnvironment); } - static Version GetVersion() { - const string versionPrefix = "KurrentDB version"; - const string esdbVersionPrefix = "EventStoreDB version"; - - using var cts = new CancellationTokenSource(FromSeconds(30)); - using var eventstore = new Builder().UseContainer() - .UseImage(GlobalEnvironment.DockerImage) - .Command("--version") - .Build() - .Start(); - - using var log = eventstore.Logs(true, cts.Token); - var logs = log.ReadToEnd(); - foreach (var line in logs) { - if (line.StartsWith(versionPrefix) && - Version.TryParse(new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), out var version)) { - return version; - } - - if (line.StartsWith(esdbVersionPrefix) && - Version.TryParse(new string(ReadVersion(line[(esdbVersionPrefix.Length + 1)..]).ToArray()), out var esdbVersion)) { - return esdbVersion; - } - } - - throw new InvalidOperationException($"Could not determine server version from logs: {string.Join(Environment.NewLine, logs)}"); + protected override ContainerBuilder Configure() { + var port = Options.DBClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; - IEnumerable ReadVersion(string s) { - foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { - yield return c; - } - } - } + var containerName = $"dotnet-client-test-{port}-{Guid.NewGuid():N}"; - protected override ContainerBuilder Configure() { - var env = Options.Environment.Select(pair => $"{pair.Key}={pair.Value}").ToArray(); - - var port = Options.DBClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; - var certsPath = Path.Combine(Environment.CurrentDirectory, "certs"); - - var containerName = $"es-client-dotnet-test-{port}-{Guid.NewGuid().ToString()[30..]}"; - - CertificatesManager.VerifyCertificatesExist(certsPath); - - var builder = new Builder() - .UseContainer() - .UseImage(Options.Environment["ES_DOCKER_IMAGE"]) - .WithName(containerName) - .WithPublicEndpointResolver() - .WithEnvironment(env) - .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) - .ExposePort(port, 2113) - .WaitUntilReadyWithConstantBackoff( - 1_000, - 60, - service => { - var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); - if (!output.Success) - throw new Exception(output.Error); - } - ); + var builder = CreateContainer(Options.Environment, containerName, port); - return builder; + return AddReadinessCheck(builder); } } @@ -167,8 +46,10 @@ protected override ContainerBuilder Configure() { /// Using the default 2113 port assumes that the test is running sequentially. /// /// -class NetworkPortProvider(int port = 2114) { - public const int DefaultEsdbPort = 2113; +internal class NetworkPortProvider(int port = 2114) { + int _port = port; + + public const int DefaultPort = 2113; static readonly SemaphoreSlim Semaphore = new(1, 1); @@ -179,7 +60,7 @@ async Task GetNextAvailablePort(TimeSpan delay = default) { using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); while (true) { - var nexPort = Interlocked.Increment(ref port); + var nexPort = Interlocked.Increment(ref _port); try { await socket.ConnectAsync(IPAddress.Any, nexPort); @@ -190,11 +71,7 @@ async Task GetNextAvailablePort(TimeSpan delay = default) { await Task.Delay(delay); } finally { -#if NET - if (socket.Connected) await socket.DisconnectAsync(true); -#else if (socket.Connected) socket.Disconnect(true); -#endif } } } finally { @@ -202,5 +79,5 @@ async Task GetNextAvailablePort(TimeSpan delay = default) { } } - public int NextAvailablePort => GetNextAvailablePort(FromMilliseconds(100)).GetAwaiter().GetResult(); + public int NextAvailablePort => GetNextAvailablePort(100.Milliseconds()).GetAwaiter().GetResult(); } diff --git a/test/KurrentDB.Client.Tests.Common/FluentDocker/TestContainerService.cs b/test/KurrentDB.Client.Tests.Common/FluentDocker/TestContainerService.cs index 2056b21fd..908563ca0 100644 --- a/test/KurrentDB.Client.Tests.Common/FluentDocker/TestContainerService.cs +++ b/test/KurrentDB.Client.Tests.Common/FluentDocker/TestContainerService.cs @@ -1,6 +1,140 @@ +// ReSharper disable InconsistentNaming + +using System.Text.RegularExpressions; using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Services; +using System.Net; +using System.Net.Sockets; +using Ductus.FluentDocker.Extensions; +using Humanizer; +using Ductus.FluentDocker.Model.Builders; +using Ductus.FluentDocker.Services.Extensions; namespace KurrentDB.Client.Tests.FluentDocker; -public abstract class TestContainerService : TestService; +public abstract class TestContainerService : TestService { + static Version? _version; + // Define the regex pattern as a static readonly field instead of using GeneratedRegex + static readonly Regex _versionRegex = new Regex(@"\b(?:KurrentDB|EventStore)\s+version\s+([0-9]+(?:\.[0-9]+)*)", RegexOptions.Compiled); + + public static Version Version => _version ??= GetVersion(); + + internal static string ConnectionString => "kurrentdb://admin:changeit@localhost:{port}/?tlsVerifyCert=false"; + + protected static ContainerBuilder CreateContainer( + IDictionary environment, string containerName = "dotnet-client-test", int port = 2113 + ) { + var env = environment.Select(pair => $"{pair.Key}={pair.Value}").ToArray(); + + var certsPath = Path.Combine(Environment.CurrentDirectory, "certs"); + + CertificatesManager.VerifyCertificatesExist(certsPath); + + return new Builder() + .UseContainer() + .UseImage(environment["TESTCONTAINER_KURRENTDB_IMAGE"]) + .WithName(containerName) + .WithPublicEndpointResolver() + .WithEnvironment(env) + .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) + .ExposePort(port, 2113); + } + + protected static ContainerBuilder AddReadinessCheck(ContainerBuilder builder) { + return builder.WaitUntilReadyWithConstantBackoff( + 1_000, + 60, + service => { + var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); + if (!output.Success) + throw new Exception(output.Error); + + var versionOutput = service.ExecuteCommand("/opt/kurrentdb/kurrentd --version"); + if (!versionOutput.Success) + versionOutput = service.ExecuteCommand("/opt/eventstore/eventstored --version"); + + if (versionOutput.Success && TryParseVersion(versionOutput.Log.FirstOrDefault(), out var version)) + _version ??= version; + } + ); + } + + /// Attempts to parse a version number from the given input string. + /// This method looks for a version pattern within the input string, such as + /// "KurrentDB version 25.1.0.2299-nightly" or "EventStore version 25.1.0.2299-nightly", + /// and tries to extract and parse the version information. + /// The input span containing the version string to parse. + /// When the method returns, contains the parsed object, or null if parsing failed. + /// true if the version could be successfully parsed; otherwise, false. + static bool TryParseVersion(ReadOnlySpan input, out Version? version) { + version = null; + + if (input.IsEmpty) { + return false; + } + + try { + // Use the static regex field instead of calling VersionRegex() method + var match = _versionRegex.Match(input.ToString()); + + if (!match.Success || match.Groups.Count < 2) + return false; + + return Version.TryParse(match.Groups[1].Value, out version); + } catch (Exception ex) { + throw new Exception($"Failed to parse version from: {input.ToString()}", ex); + } + } + + // static Version GetVersion() { + // using var cts = new CancellationTokenSource(30.Seconds()); + // using var database = new Builder().UseContainer() + // .UseImage(GlobalEnvironment.DockerImage) + // .Command("--version") + // .Build() + // .Start(); + // + // using var log = database.Logs(true, cts.Token); + // + // foreach (var line in log.ReadToEnd()) + // if (TryParseVersion(line, out var version)) + // return version; + // + // throw new InvalidOperationException("Could not determine server version from logs"); + // } + + static Version GetVersion() { + const string versionPrefix = "KurrentDB version"; + const string esdbVersionPrefix = "EventStoreDB version"; + + using var cts = new CancellationTokenSource(30.Seconds()); + using var eventstore = new Builder().UseContainer() + .UseImage(GlobalEnvironment.DockerImage) + .Command("--version") + .Build() + .Start(); + + using var log = eventstore.Logs(true, cts.Token); + var logs = log.ReadToEnd(); + foreach (var line in logs) { + if (line.StartsWith(versionPrefix) && + Version.TryParse(new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), out var version)) { + return version; + } + + if (line.StartsWith(esdbVersionPrefix) && + Version.TryParse(new string(ReadVersion(line[(esdbVersionPrefix.Length + 1)..]).ToArray()), out var esdbVersion)) { + return esdbVersion; + } + } + + throw new InvalidOperationException($"Could not determine server version from logs: {string.Join(Environment.NewLine, logs)}"); + + IEnumerable ReadVersion(string s) { + foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { + yield return c; + } + } + } + +} diff --git a/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs b/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs index 6934aface..4f48787ba 100644 --- a/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs +++ b/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs @@ -1,41 +1,46 @@ using System.Collections.Immutable; -using KurrentDB.Client; +using Humanizer; using Microsoft.Extensions.Configuration; namespace KurrentDB.Client.Tests; public static class GlobalEnvironment { + public static double MaxAppendSize => 2.Megabytes().Bytes; + static GlobalEnvironment() { EnsureDefaults(Application.Configuration); UseCluster = Application.Configuration.GetValue("ES_USE_CLUSTER"); UseExternalServer = Application.Configuration.GetValue("ES_USE_EXTERNAL_SERVER"); - DockerImage = Application.Configuration.GetValue("ES_DOCKER_IMAGE")!; + DockerImage = Application.Configuration.GetValue("TESTCONTAINER_KURRENTDB_IMAGE")!; Variables = Application.Configuration.AsEnumerable() - .Where(x => x.Key.StartsWith("ES_") || x.Key.StartsWith("EVENTSTORE_")) + .Where(x => x.Key.StartsWith("TESTCONTAINER_") || x.Key.StartsWith("EVENTSTORE_") || x.Key.StartsWith("KURRENTDB_")) .OrderBy(x => x.Key) .ToImmutableDictionary(x => x.Key, x => x.Value ?? string.Empty)!; return; static void EnsureDefaults(IConfiguration configuration) { - configuration.EnsureValue("ES_USE_CLUSTER", "false"); - configuration.EnsureValue("ES_USE_EXTERNAL_SERVER", "false"); - - configuration.EnsureValue("ES_DOCKER_REGISTRY", "docker.kurrent.io/eventstore/eventstoredb-ee"); - configuration.EnsureValue("ES_DOCKER_TAG", "lts"); - configuration.EnsureValue("ES_DOCKER_IMAGE", $"{configuration["ES_DOCKER_REGISTRY"]}:{configuration["ES_DOCKER_TAG"]}"); + // internal defaults + configuration.EnsureValue("TESTCONTAINER_KURRENTDB_IMAGE", "docker.cloudsmith.io/eventstore/kurrent-staging/kurrentdb:ci"); + // database defaults configuration.EnsureValue("EVENTSTORE_TELEMETRY_OPTOUT", "true"); configuration.EnsureValue("EVENTSTORE_ALLOW_UNKNOWN_OPTIONS", "true"); - configuration.EnsureValue("EVENTSTORE_MEM_DB", "false"); configuration.EnsureValue("EVENTSTORE_RUN_PROJECTIONS", "None"); configuration.EnsureValue("EVENTSTORE_START_STANDARD_PROJECTIONS", "false"); - configuration.EnsureValue("EVENTSTORE_LOG_LEVEL", "Information"); - configuration.EnsureValue("EVENTSTORE_DISABLE_LOG_FILE", "true"); + configuration.EnsureValue("EVENTSTORE_MEM_DB", "true"); + configuration.EnsureValue("EVENTSTORE_CERTIFICATE_FILE", "/etc/eventstore/certs/node/node.crt"); + configuration.EnsureValue("EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE", "/etc/eventstore/certs/node/node.key"); configuration.EnsureValue("EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH", "/etc/eventstore/certs/ca"); + configuration.EnsureValue("EVENTSTORE__PLUGINS__USERCERTIFICATES__ENABLED", "true"); + configuration.EnsureValue("EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE", "10000"); + configuration.EnsureValue("EVENTSTORE_STREAM_INFO_CACHE_CAPACITY", "10000"); configuration.EnsureValue("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", "true"); + configuration.EnsureValue("EVENTSTORE_LOG_LEVEL", "Default"); + configuration.EnsureValue("EVENTSTORE_DISABLE_LOG_FILE", "true"); + configuration.EnsureValue("EVENTSTORE_MAX_APPEND_SIZE", $"{MaxAppendSize}"); } } diff --git a/test/KurrentDB.Client.Tests.Common/KurrentDB.Client.Tests.Common.csproj b/test/KurrentDB.Client.Tests.Common/KurrentDB.Client.Tests.Common.csproj index 6b1d9c628..29eb6b77c 100644 --- a/test/KurrentDB.Client.Tests.Common/KurrentDB.Client.Tests.Common.csproj +++ b/test/KurrentDB.Client.Tests.Common/KurrentDB.Client.Tests.Common.csproj @@ -45,9 +45,6 @@ Always - - Always - Always diff --git a/test/KurrentDB.Client.Tests.Common/shared.env b/test/KurrentDB.Client.Tests.Common/shared.env deleted file mode 100644 index f753bc9c7..000000000 --- a/test/KurrentDB.Client.Tests.Common/shared.env +++ /dev/null @@ -1,15 +0,0 @@ -EVENTSTORE_CLUSTER_SIZE=3 -EVENTSTORE_INT_TCP_PORT=1112 -EVENTSTORE_HTTP_PORT=2113 -EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH=/etc/eventstore/certs/ca -EVENTSTORE_DISCOVER_VIA_DNS=false -EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=false -EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE=10000 - -# pass through from environment -EVENTSTORE_DB_LOG_FORMAT -EVENTSTORE_LOG_LEVEL -EVENTSTORE_MAX_APPEND_SIZE -EVENTSTORE_MEM_DB -EVENTSTORE_RUN_PROJECTIONS -EVENTSTORE_START_STANDARD_PROJECTIONS diff --git a/test/KurrentDB.Client.Tests/Streams/AppendTests.cs b/test/KurrentDB.Client.Tests/Streams/AppendTests.cs index b607020e4..405a80002 100644 --- a/test/KurrentDB.Client.Tests/Streams/AppendTests.cs +++ b/test/KurrentDB.Client.Tests/Streams/AppendTests.cs @@ -485,21 +485,21 @@ public async Task succeeds_when_size_is_less_than_max_append_size() { [RetryFact] public async Task fails_when_size_exceeds_max_append_size() { // Arrange - var maxAppendSize = (uint)100.Kilobytes().Bytes; - var stream = Fixture.GetStreamName(); - var eventsAppendSize = (uint)10.Megabytes().Bytes; + var stream = Fixture.GetStreamName(); + + var maxAppendSize = (uint)10.Megabytes().Bytes; // Act - var (events, size) = Fixture.CreateTestEventsUpToMaxSize(eventsAppendSize); + var (events, size) = Fixture.CreateTestEventsUpToMaxSize(maxAppendSize); // Assert - size.ShouldBeGreaterThan(maxAppendSize); + size.ShouldBeGreaterThan((uint)GlobalEnvironment.MaxAppendSize); var ex = await Fixture.Streams .AppendToStreamAsync(stream, StreamState.NoStream, events) .ShouldThrowAsync(); - ex.MaxAppendSize.ShouldBe(maxAppendSize); + ex.MaxAppendSize.ShouldBe((uint)GlobalEnvironment.MaxAppendSize); } [RetryFact] diff --git a/test/KurrentDB.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs b/test/KurrentDB.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs index 36b2948dd..e3cd258b1 100644 --- a/test/KurrentDB.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs +++ b/test/KurrentDB.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs @@ -267,14 +267,14 @@ public async Task stream_found() { ).Messages.ToArrayAsync(); Assert.Equal( - eventCount + (Fixture.EventStoreHasLastStreamPosition ? 2 : 1), + eventCount + (Fixture.HasLastStreamPosition ? 2 : 1), result.Length ); Assert.Equal(StreamMessage.Ok.Instance, result[0]); Assert.Equal(eventCount, result.OfType().Count()); - if (Fixture.EventStoreHasLastStreamPosition) + if (Fixture.HasLastStreamPosition) Assert.Equal(new StreamMessage.LastStreamPosition(new(31)), result[^1]); } } diff --git a/test/KurrentDB.Client.Tests/Streams/Read/ReadStreamForwardTests.cs b/test/KurrentDB.Client.Tests/Streams/Read/ReadStreamForwardTests.cs index 05281ddc4..0a26451ac 100644 --- a/test/KurrentDB.Client.Tests/Streams/Read/ReadStreamForwardTests.cs +++ b/test/KurrentDB.Client.Tests/Streams/Read/ReadStreamForwardTests.cs @@ -208,7 +208,7 @@ public async Task stream_found() { ).Messages.ToArrayAsync(); Assert.Equal( - eventCount + (Fixture.EventStoreHasLastStreamPosition ? 2 : 1), + eventCount + (Fixture.HasLastStreamPosition ? 2 : 1), result.Length ); @@ -216,10 +216,10 @@ public async Task stream_found() { Assert.Equal(eventCount, result.OfType().Count()); var first = Assert.IsType(result[1]); Assert.Equal(new(0), first.ResolvedEvent.OriginalEventNumber); - var last = Assert.IsType(result[Fixture.EventStoreHasLastStreamPosition ? ^2 : ^1]); + var last = Assert.IsType(result[Fixture.HasLastStreamPosition ? ^2 : ^1]); Assert.Equal(new((ulong)eventCount - 1), last.ResolvedEvent.OriginalEventNumber); - if (Fixture.EventStoreHasLastStreamPosition) + if (Fixture.HasLastStreamPosition) Assert.Equal( new StreamMessage.LastStreamPosition(new((ulong)eventCount - 1)), result[^1] @@ -249,18 +249,18 @@ await Fixture.Streams.SetStreamMetadataAsync( ).Messages.ToArrayAsync(); Assert.Equal( - eventCount - 32 + (Fixture.EventStoreHasLastStreamPosition ? 3 : 1), + eventCount - 32 + (Fixture.HasLastStreamPosition ? 3 : 1), result.Length ); Assert.Equal(StreamMessage.Ok.Instance, result[0]); - if (Fixture.EventStoreHasLastStreamPosition) + if (Fixture.HasLastStreamPosition) Assert.Equal(new StreamMessage.FirstStreamPosition(new(32)), result[1]); Assert.Equal(32, result.OfType().Count()); - if (Fixture.EventStoreHasLastStreamPosition) + if (Fixture.HasLastStreamPosition) Assert.Equal( new StreamMessage.LastStreamPosition(new((ulong)eventCount - 1)), result[^1] diff --git a/test/KurrentDB.Client.Tests/UserManagement/ListUserTests.cs b/test/KurrentDB.Client.Tests/UserManagement/ListUserTests.cs index ed1318135..4218877de 100644 --- a/test/KurrentDB.Client.Tests/UserManagement/ListUserTests.cs +++ b/test/KurrentDB.Client.Tests/UserManagement/ListUserTests.cs @@ -1,16 +1,15 @@ -using KurrentDB.Client; +// ReSharper disable InconsistentNaming namespace KurrentDB.Client.Tests; [Trait("Category", "Target:UserManagement")] public class ListUserTests(ITestOutputHelper output, KurrentDBPermanentFixture fixture) : KurrentDBPermanentTests(output, fixture) { - readonly string _userFullNamePrefix = fixture.IsKdb ? "KurrentDB" : "Event Store"; - [Fact] + [Fact(Skip = "Temporary")] public async Task returns_all_created_users() { var seed = await Fixture.CreateTestUsers(); - var admin = new UserDetails("admin", $"{_userFullNamePrefix} Administrator", new[] { "$admins" }, false, default); - var ops = new UserDetails("ops", $"{_userFullNamePrefix} Operations", new[] { "$ops" }, false, default); + var admin = new UserDetails("admin", "KurrentDB Administrator", ["$admins"], false, null); + var ops = new UserDetails("ops", "KurrentDB Operations", ["$ops"], false, null); var expected = new[] { admin, ops } .Concat(seed.Select(user => user.Details)) @@ -18,24 +17,24 @@ public async Task returns_all_created_users() { var actual = await Fixture.DBUsers .ListAllAsync(userCredentials: TestCredentials.Root) - .Select(user => new UserDetails(user.LoginName, user.FullName, user.Groups, user.Disabled, default)) + .Select(user => new UserDetails(user.LoginName, user.FullName, user.Groups, user.Disabled, null)) .ToArrayAsync(); - expected.ShouldBeSubsetOf(actual); + foreach (var user in expected) actual.ShouldContain(user); } - [Fact] + [Fact(Skip = "Temporary")] public async Task returns_all_system_users() { - var admin = new UserDetails("admin", $"{_userFullNamePrefix} Administrator", new[] { "$admins" }, false, default); - var ops = new UserDetails("ops", $"{_userFullNamePrefix} Operations", new[] { "$ops" }, false, default); + var admin = new UserDetails("admin", "KurrentDB Administrator", ["$admins"], false, null); + var ops = new UserDetails("ops", "KurrentDB Operations", ["$ops"], false, null); var expected = new[] { admin, ops }; var actual = await Fixture.DBUsers .ListAllAsync(userCredentials: TestCredentials.Root) - .Select(user => new UserDetails(user.LoginName, user.FullName, user.Groups, user.Disabled, default)) + .Select(user => new UserDetails(user.LoginName, user.FullName, user.Groups, user.Disabled, null)) .ToArrayAsync(); - expected.ShouldBeSubsetOf(actual); + foreach (var user in expected) actual.ShouldContain(user); } } From 51c576f55fcd9f5c4a30faf1d98bd51dcb7e8289 Mon Sep 17 00:00:00 2001 From: William Chong Date: Mon, 11 Aug 2025 10:53:49 +0400 Subject: [PATCH 07/29] Add multistream append method --- samples/appending-events/Program.cs | 48 +++++++ src/KurrentDB.Client/Core/Common/Constants.cs | 14 +- .../Core/DynamicValueMapper.cs | 40 ++++++ .../TransactionMaxSizeExceededException.cs | 17 +++ .../WrongExpectedVersionException.cs | 8 +- .../Core/GrpcServerCapabilitiesClient.cs | 7 +- .../Interceptors/RequiresLeaderInterceptor.cs | 67 ++++++++++ .../Exceptions/Utf8JsonReaderExtensions.cs | 14 ++ .../Core/Internal/SystemTypes.cs | 60 +++++++++ .../Core/KurrentDBClientBase.cs | 1 + .../Core/ServerCapabilities.cs | 1 + .../protocol/v2/streams/streams.proto | 2 - .../Json/JsonSchemaSerializerOptions.cs | 27 ++++ .../Serialization/Json/JsonSerializer.cs | 31 +++++ .../Streams/KurrentDBClient.MultiAppend.cs | 68 ++++++++++ .../Model/Metadata/MetadataJsonConverter.cs | 75 +++++++++++ .../Streams/Streams/Model/SchemaDataFormat.cs | 19 +++ .../Streams/Model/StreamsClientMapper.cs | 79 +++++++++++ .../Streams/Model/StreamsClientModel.cs | 74 ++++++++++ .../Facts/MinimumVersion.cs | 112 ++++++++++++++++ .../Streams/MultiStreamAppendTests.cs | 126 ++++++++++++++++++ 21 files changed, 880 insertions(+), 10 deletions(-) create mode 100644 src/KurrentDB.Client/Core/DynamicValueMapper.cs create mode 100644 src/KurrentDB.Client/Core/Exceptions/TransactionMaxSizeExceededException.cs create mode 100644 src/KurrentDB.Client/Core/Interceptors/RequiresLeaderInterceptor.cs create mode 100644 src/KurrentDB.Client/Core/Internal/Exceptions/Utf8JsonReaderExtensions.cs create mode 100644 src/KurrentDB.Client/Core/Internal/SystemTypes.cs create mode 100644 src/KurrentDB.Client/Schema/Serialization/Json/JsonSchemaSerializerOptions.cs create mode 100644 src/KurrentDB.Client/Schema/Serialization/Json/JsonSerializer.cs create mode 100644 src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs create mode 100644 src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataJsonConverter.cs create mode 100644 src/KurrentDB.Client/Streams/Streams/Model/SchemaDataFormat.cs create mode 100644 src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs create mode 100644 src/KurrentDB.Client/Streams/Streams/Model/StreamsClientModel.cs create mode 100644 test/KurrentDB.Client.Tests.Common/Facts/MinimumVersion.cs create mode 100644 test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs diff --git a/samples/appending-events/Program.cs b/samples/appending-events/Program.cs index 5b344434b..558ef0992 100644 --- a/samples/appending-events/Program.cs +++ b/samples/appending-events/Program.cs @@ -6,6 +6,7 @@ await using var client = new KurrentDBClient(settings); +await MultiStreamAppend(client); await AppendToStream(client); await AppendWithConcurrencyCheck(client); await AppendWithNoStream(client); @@ -13,6 +14,51 @@ return; +static async Task MultiStreamAppend(KurrentDBClient client) { + var serializer = new KurrentDB.Client.Schema.Serialization.Json.JsonSerializer(); + + var metadata = serializer.Serialize( + new { + TimeStamp = DateTime.UtcNow, + } + ); + + AppendStreamRequest[] requests = [ + new( + "stream-one", + StreamState.NoStream, + [new EventData(Uuid.NewUuid(), "event-one", "hello"u8.ToArray(), metadata)] + ), + new( + "stream-two", + StreamState.NoStream, + [new EventData(Uuid.NewUuid(), "event-one", "hello"u8.ToArray(), metadata)] + ) + ]; + + var result = await client.MultiStreamAppendAsync(requests.ToAsyncEnumerable()); + + if (result is MultiAppendSuccess { Successes: var successes }) + foreach (var item in successes) + Console.WriteLine($"Stream: {item.Stream}, Position: {item.Position}"); + + else if (result is MultiAppendFailure { Failures: var failures }) + foreach (var item in failures) + switch (item) { + case WrongExpectedVersionException ex: + Console.WriteLine($"Stream: {ex.StreamName}, Expected revision: {ex.ActualStreamState}"); + break; + + case TransactionMaxSizeExceededException ex: + Console.WriteLine($"Transaction size exceeded the maximum allowed size of {ex.MaxSize} bytes."); + break; + + case StreamDeletedException ex: + Console.WriteLine($"Stream: {ex.Stream} has been deleted."); + break; + } +} + static async Task AppendToStream(KurrentDBClient client) { #region append-to-stream @@ -167,3 +213,5 @@ await client.AppendToStreamAsync( #endregion overriding-user-credentials } + +record Metadata(DateTime TimeStamp); diff --git a/src/KurrentDB.Client/Core/Common/Constants.cs b/src/KurrentDB.Client/Core/Common/Constants.cs index b58bdee63..925b01619 100644 --- a/src/KurrentDB.Client/Core/Common/Constants.cs +++ b/src/KurrentDB.Client/Core/Common/Constants.cs @@ -39,11 +39,15 @@ public static class Exceptions { } public static class Metadata { - public const string Type = "type"; - public const string Created = "created"; - public const string ContentType = "content-type"; - - public static readonly string[] RequiredMetadata = [Type, ContentType]; + const string SystemPrefix = "$"; + + public const string Type = "type"; + public const string Created = "created"; + public const string ContentType = "content-type"; + public const string SchemaName = $"{SystemPrefix}schema.name"; + public const string SchemaDataFormat = $"{SystemPrefix}schema.data-format"; + + public static readonly string[] RequiredMetadata = [Type, ContentType]; public static class ContentTypes { public const string ApplicationJson = "application/json"; diff --git a/src/KurrentDB.Client/Core/DynamicValueMapper.cs b/src/KurrentDB.Client/Core/DynamicValueMapper.cs new file mode 100644 index 000000000..cd2dd5fa9 --- /dev/null +++ b/src/KurrentDB.Client/Core/DynamicValueMapper.cs @@ -0,0 +1,40 @@ +using Google.Protobuf; +using Google.Protobuf.Collections; +using Google.Protobuf.WellKnownTypes; +using JetBrains.Annotations; +using KurrentDB.Protocol; + +namespace KurrentDB.Client; + +[PublicAPI] +static class DynamicValueMapper { + public static MapField MapToDynamicMapField(this Dictionary source) => + source.Aggregate( + new MapField(), + (seed, entry) => { + seed.Add(entry.Key, MapToDynamicValue(entry.Value)); + return seed; + } + ); + + public static DynamicValue MapToDynamicValue(this object? source) { + return source switch { + null => new() { NullValue = NullValue.NullValue }, + string x => new() { StringValue = x }, + bool x => new() { BooleanValue = x }, + int x => new() { Int32Value = x }, + long x => new() { Int64Value = x }, + float x => new() { FloatValue = x }, + double x => new() { DoubleValue = x }, + + DateTime x => new() { TimestampValue = Timestamp.FromDateTime(x.Kind is DateTimeKind.Utc ? x : DateTime.SpecifyKind(x, DateTimeKind.Utc)) }, + DateTimeOffset x => new() { TimestampValue = Timestamp.FromDateTimeOffset(x) }, + TimeSpan x => new() { DurationValue = x.ToDuration() }, + + byte[] x => new() { BytesValue = ByteString.CopyFrom(x) }, + ReadOnlyMemory x => new() { BytesValue = ByteString.CopyFrom(x.Span) }, + + _ => new() { StringValue = source.ToString() } // any other type is converted to string + }; + } +} diff --git a/src/KurrentDB.Client/Core/Exceptions/TransactionMaxSizeExceededException.cs b/src/KurrentDB.Client/Core/Exceptions/TransactionMaxSizeExceededException.cs new file mode 100644 index 000000000..3f46a9689 --- /dev/null +++ b/src/KurrentDB.Client/Core/Exceptions/TransactionMaxSizeExceededException.cs @@ -0,0 +1,17 @@ +namespace KurrentDB.Client; + +#pragma warning disable CS8509 +/// +/// Exception thrown when a transaction exceeds the allowed maximum size limit. +/// +public class TransactionMaxSizeExceededException : Exception { + /// + /// The maximum size, in bytes, allowed for a transaction before it is considered invalid. + /// + public readonly long MaxSize; + + public TransactionMaxSizeExceededException(long maxSize, Exception? exception = null) + : base($"Transaction size exceeded the maximum allowed size of {maxSize} bytes.", exception) { + MaxSize = maxSize; + } +} diff --git a/src/KurrentDB.Client/Core/Exceptions/WrongExpectedVersionException.cs b/src/KurrentDB.Client/Core/Exceptions/WrongExpectedVersionException.cs index 0736dc84c..71eab9678 100644 --- a/src/KurrentDB.Client/Core/Exceptions/WrongExpectedVersionException.cs +++ b/src/KurrentDB.Client/Core/Exceptions/WrongExpectedVersionException.cs @@ -1,5 +1,3 @@ -using System; - namespace KurrentDB.Client { /// /// Exception thrown if the expected version specified on an operation @@ -45,5 +43,11 @@ public WrongExpectedVersionException(string streamName, StreamState expectedStre ExpectedVersion = expectedStreamState.ToInt64(); ActualVersion = actualStreamState.ToInt64(); } + + public WrongExpectedVersionException(string streamName, StreamState actualStreamState, Exception? exception = null, string? message = null) : + base(message ?? $"Append failed due to WrongExpectedVersion. Stream: {streamName}, Actual version: {actualStreamState}", exception) { + StreamName = streamName; + ActualStreamState = actualStreamState; + } } } diff --git a/src/KurrentDB.Client/Core/GrpcServerCapabilitiesClient.cs b/src/KurrentDB.Client/Core/GrpcServerCapabilitiesClient.cs index ee56e9527..24a10fd0c 100644 --- a/src/KurrentDB.Client/Core/GrpcServerCapabilitiesClient.cs +++ b/src/KurrentDB.Client/Core/GrpcServerCapabilitiesClient.cs @@ -29,6 +29,7 @@ public async Task GetAsync( var supportsPersistentSubscriptionsRestartSubsystem = false; var supportsPersistentSubscriptionsReplayParked = false; var supportsPersistentSubscriptionsList = false; + var supportsMultiStreamAppend = false; var response = await call.ResponseAsync.ConfigureAwait(false); @@ -52,6 +53,9 @@ public async Task GetAsync( case ("event_store.client.persistent_subscriptions.persistentsubscriptions", "list"): supportsPersistentSubscriptionsList = true; continue; + case ("kurrentdb.protocol.v2.streamsservice", "multistreamappend"): + supportsMultiStreamAppend = true; + continue; } } @@ -61,7 +65,8 @@ public async Task GetAsync( SupportsPersistentSubscriptionsGetInfo: supportsPersistentSubscriptionsGetInfo, SupportsPersistentSubscriptionsRestartSubsystem: supportsPersistentSubscriptionsRestartSubsystem, SupportsPersistentSubscriptionsReplayParked: supportsPersistentSubscriptionsReplayParked, - SupportsPersistentSubscriptionsList: supportsPersistentSubscriptionsList); + SupportsPersistentSubscriptionsList: supportsPersistentSubscriptionsList, + SupportsMultiStreamAppend: supportsMultiStreamAppend); } catch (Exception ex) when (ex.GetBaseException() is RpcException rpcException && rpcException.StatusCode == StatusCode.Unimplemented) { diff --git a/src/KurrentDB.Client/Core/Interceptors/RequiresLeaderInterceptor.cs b/src/KurrentDB.Client/Core/Interceptors/RequiresLeaderInterceptor.cs new file mode 100644 index 000000000..1073cfd5a --- /dev/null +++ b/src/KurrentDB.Client/Core/Interceptors/RequiresLeaderInterceptor.cs @@ -0,0 +1,67 @@ +// ReSharper disable InconsistentNaming + +using Grpc.Core; +using Grpc.Core.Interceptors; +using static System.StringComparer; + +namespace KurrentDB.Client.Interceptors; + +sealed class RequiresLeaderInterceptor(params string[]? excludedOperations) : Interceptor { + readonly HashSet ExcludedOperations = new(excludedOperations ?? [], OrdinalIgnoreCase); + + public override AsyncUnaryCall AsyncUnaryCall( + TRequest request, + ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation + ) where TRequest : class where TResponse : class => + continuation(request, PrepareContext(context)); + + public override AsyncClientStreamingCall AsyncClientStreamingCall( + ClientInterceptorContext context, + AsyncClientStreamingCallContinuation continuation + ) where TRequest : class where TResponse : class => + continuation(PrepareContext(context)); + + public override AsyncServerStreamingCall AsyncServerStreamingCall( + TRequest request, + ClientInterceptorContext context, + AsyncServerStreamingCallContinuation continuation + ) where TRequest : class where TResponse : class => + continuation(request, PrepareContext(context)); + + public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( + ClientInterceptorContext context, + AsyncDuplexStreamingCallContinuation continuation + ) where TRequest : class where TResponse : class => + continuation(PrepareContext(context)); + + ClientInterceptorContext PrepareContext( + ClientInterceptorContext context + ) where TRequest : class where TResponse : class { + var operationName = Path.GetFileName(context.Method.Name); + + return ExcludedOperations.Contains(operationName) + ? RemoveRequiresLeaderHeader(context) + : context; + } + + static ClientInterceptorContext RemoveRequiresLeaderHeader( + ClientInterceptorContext context + ) where TRequest : class where TResponse : class { + if (context.Options.Headers is null) + return context; + + var headers = new Metadata(); + + context.Options.Headers + .Where(header => header.Key is not Constants.Headers.RequiresLeader) + .ToList() + .ForEach(headers.Add); + + return new ClientInterceptorContext( + context.Method, + context.Host, + context.Options.WithHeaders(headers) + ); + } +} diff --git a/src/KurrentDB.Client/Core/Internal/Exceptions/Utf8JsonReaderExtensions.cs b/src/KurrentDB.Client/Core/Internal/Exceptions/Utf8JsonReaderExtensions.cs new file mode 100644 index 000000000..175cb03d8 --- /dev/null +++ b/src/KurrentDB.Client/Core/Internal/Exceptions/Utf8JsonReaderExtensions.cs @@ -0,0 +1,14 @@ +using System.Globalization; +using System.Text.Json; + +namespace KurrentDB.Client.Core.Internal.Exceptions; + +static class Utf8JsonReaderExtensions { + public static bool TryGetTimeSpan(this ref Utf8JsonReader reader, out TimeSpan value) { + if (reader.TokenType == JsonTokenType.String && TimeSpan.TryParse(reader.GetString(), CultureInfo.InvariantCulture, out value)) + return true; + + value = TimeSpan.Zero; + return false; + } +} diff --git a/src/KurrentDB.Client/Core/Internal/SystemTypes.cs b/src/KurrentDB.Client/Core/Internal/SystemTypes.cs new file mode 100644 index 000000000..683a135f9 --- /dev/null +++ b/src/KurrentDB.Client/Core/Internal/SystemTypes.cs @@ -0,0 +1,60 @@ +using System.Diagnostics; + +namespace KurrentDB.Client.Core.Internal; + +/// +/// Provides functionality to resolve and retrieve types by their full names, +/// searching the currently loaded assemblies or scanned assemblies. +/// +static class SystemTypes { + public static readonly Type MissingType = Type.Missing.GetType(); + + public static bool IsBytes(this object value) => value.GetType().IsBytes(); + + public static bool IsBytes(this Type type) => + type == typeof(byte[]) || + type == typeof(ReadOnlyMemory) || + type == typeof(Memory); + + /// + /// Checks if the specified type is the placeholder type representing a missing type. + /// + /// + /// The to check. + /// + /// + /// true if the type is the placeholder for a missing type; otherwise, false. + /// + [DebuggerStepThrough] + public static bool IsMissing(this Type source) => source == MissingType; + + /// + /// Determines whether the specified type is an instantiable class. + /// + /// The type to evaluate. + /// true if the type is a non-abstract class; otherwise, false. + public static bool IsInstantiableClass(this Type type) => type is { IsClass: true, IsAbstract: false }; + + /// + /// Determines whether the type's full name matches the specified name. + /// + /// The type to check. + /// The full name to compare against. + /// true if the type's full name matches the specified name; otherwise, false. + public static bool MatchesFullName(this Type type, string name) => type.FullName?.Equals(name, StringComparison.OrdinalIgnoreCase) ?? false; + + /// + /// Determines whether the namespace of the specified matches the given namespace prefix. + /// + /// + /// The whose namespace is to be compared. + /// + /// + /// The namespace prefix to compare with the namespace of the specified type. + /// + /// + /// true if the namespace of the specified type matches the namespace prefix; otherwise, false. + /// + public static bool MatchesNamespace(this Type type, string namespacePrefix) => + type.Namespace?.Equals(namespacePrefix, StringComparison.OrdinalIgnoreCase) ?? false; +} diff --git a/src/KurrentDB.Client/Core/KurrentDBClientBase.cs b/src/KurrentDB.Client/Core/KurrentDBClientBase.cs index 6fb8d704a..03b7c2364 100644 --- a/src/KurrentDB.Client/Core/KurrentDBClientBase.cs +++ b/src/KurrentDB.Client/Core/KurrentDBClientBase.cs @@ -63,6 +63,7 @@ CancellationToken cancellationToken var invoker = channel.CreateCallInvoker() .Intercept(new TypedExceptionInterceptor(_exceptionMap)) .Intercept(new ConnectionNameInterceptor(ConnectionName)) + .Intercept(new RequiresLeaderInterceptor("MultiStreamAppendSession")) .Intercept(new ReportLeaderInterceptor(onReconnectionRequired)); if (Settings.Interceptors is not null) { diff --git a/src/KurrentDB.Client/Core/ServerCapabilities.cs b/src/KurrentDB.Client/Core/ServerCapabilities.cs index 076bcc050..f59cf52dd 100644 --- a/src/KurrentDB.Client/Core/ServerCapabilities.cs +++ b/src/KurrentDB.Client/Core/ServerCapabilities.cs @@ -6,5 +6,6 @@ public record ServerCapabilities( bool SupportsPersistentSubscriptionsGetInfo = false, bool SupportsPersistentSubscriptionsRestartSubsystem = false, bool SupportsPersistentSubscriptionsReplayParked = false, + bool SupportsMultiStreamAppend = false, bool SupportsPersistentSubscriptionsList = false); } diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto index 1c12abf63..9dbc08222 100644 --- a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto @@ -121,8 +121,6 @@ message AppendStreamFailure { CoreErrorDetails.AccessDenied access_denied = 3; // Failed because the target stream has been deleted. StreamsErrorDetails.StreamDeleted stream_deleted = 4; - // Failed because the stream was not found. - StreamsErrorDetails.StreamNotFound stream_not_found = 5; // Failed because the transaction exceeded the maximum size allowed StreamsErrorDetails.TransactionMaxSizeExceeded transaction_max_size_exceeded = 6; } diff --git a/src/KurrentDB.Client/Schema/Serialization/Json/JsonSchemaSerializerOptions.cs b/src/KurrentDB.Client/Schema/Serialization/Json/JsonSchemaSerializerOptions.cs new file mode 100644 index 000000000..62e454eae --- /dev/null +++ b/src/KurrentDB.Client/Schema/Serialization/Json/JsonSchemaSerializerOptions.cs @@ -0,0 +1,27 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; + +namespace KurrentDB.Client.Schema.Serialization.Json; + +[PublicAPI] +public record JsonSchemaSerializerOptions { + public static readonly JsonSchemaSerializerOptions Default = new(); + + public static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new(JsonSerializerOptions.Default) { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + Converters = { + new MetadataJsonConverter(), + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) + } + }; + + public JsonSerializerOptions JsonSerializerOptions { get; init; } = DefaultJsonSerializerOptions; + public bool UseProtobufFormatter { get; init; } = true; +} diff --git a/src/KurrentDB.Client/Schema/Serialization/Json/JsonSerializer.cs b/src/KurrentDB.Client/Schema/Serialization/Json/JsonSerializer.cs new file mode 100644 index 000000000..215ce1fb4 --- /dev/null +++ b/src/KurrentDB.Client/Schema/Serialization/Json/JsonSerializer.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using KurrentDB.Client.Core.Internal; + +namespace KurrentDB.Client.Schema.Serialization.Json; + +/// +/// A serializer class that supports serialization and deserialization of objects +/// +public class JsonSerializer(JsonSerializerOptions? options = null) { + JsonSerializerOptions Options { get; } = options ?? JsonSchemaSerializerOptions.DefaultJsonSerializerOptions; + + public ReadOnlyMemory Serialize(object? value) => + System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value, Options); + + public object? Deserialize(ReadOnlyMemory data, Type type) => + !type.IsMissing() + ? System.Text.Json.JsonSerializer.Deserialize(data.Span, type, Options) + : System.Text.Json.JsonSerializer.Deserialize(data.Span, Options); + + public async ValueTask Deserialize(Stream data, Type type, CancellationToken cancellationToken = default) { + var value = !type.IsMissing() + ? await System.Text.Json.JsonSerializer.DeserializeAsync(data, type, Options, cancellationToken).ConfigureAwait(false) + : await System.Text.Json.JsonSerializer.DeserializeAsync(data, Options, cancellationToken).ConfigureAwait(false); + + return value; + } + + public T? Deserialize(ReadOnlyMemory data) where T : class => + Deserialize(data, typeof(T)) as T; +} diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs new file mode 100644 index 000000000..17397c229 --- /dev/null +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs @@ -0,0 +1,68 @@ +// ReSharper disable SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault +// ReSharper disable ConvertToPrimaryConstructor + +#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). + +using static KurrentDB.Protocol.Streams.V2.StreamsService; +using static KurrentDB.Protocol.Streams.V2.MultiStreamAppendResponse; +using Contracts = KurrentDB.Protocol.Streams.V2; + +namespace KurrentDB.Client; + +public partial class KurrentDBClient { + /// + /// Appends events to multiple streams asynchronously, ensuring the specified state of each stream is respected. + /// + /// An asynchronous enumerable of objects, each containing details of the stream, expected stream state, and events to append. + /// An optional cancellation token to observe while waiting for the operation to complete. + /// + /// A task that represents the asynchronous operation, with a result of type , indicating the outcome of the operation. + /// + /// On success, returns containing the successful append results. + /// On failure, returns containing a collection of exceptions that may include: + /// , , , or . + /// + /// + /// Thrown if the server does not support multi-stream append functionality (requires server version 25.1 or higher). + public async ValueTask MultiStreamAppendAsync( + IAsyncEnumerable requests, CancellationToken cancellationToken = default + ) { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + + if (!channelInfo.ServerCapabilities.SupportsMultiStreamAppend) + throw new InvalidOperationException($"{nameof(MultiStreamAppendAsync)} requires server version 25.1 or higher."); + + var client = new StreamsServiceClient(channelInfo.CallInvoker); + + using var session = client.MultiStreamAppendSession(KurrentDBCallOptions.CreateStreaming(Settings, cancellationToken: cancellationToken)); + + await foreach (var request in requests.WithCancellation(cancellationToken)) { + var records = await request.Messages + .Map() + .ToArrayAsync(cancellationToken) + .ConfigureAwait(false); + + var serviceRequest = new Contracts.AppendStreamRequest { + Stream = request.Stream, + ExpectedRevision = request.ExpectedState.ToInt64(), + Records = { records } + }; + + // Cancellation of stream writes is not supported by this gRPC implementation. + // To cancel the operation, we should cancel the entire session. + await session.RequestStream + .WriteAsync(serviceRequest) + .ConfigureAwait(false); + } + + await session.RequestStream.CompleteAsync(); + + var response = await session.ResponseAsync; + + return response.ResultCase switch { + ResultOneofCase.Success => new MultiAppendSuccess(response.Success.Map()), + ResultOneofCase.Failure => new MultiAppendFailure(response.Failure.Map()) + }; + } +} + diff --git a/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataJsonConverter.cs b/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataJsonConverter.cs new file mode 100644 index 000000000..8448b019c --- /dev/null +++ b/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataJsonConverter.cs @@ -0,0 +1,75 @@ +// ReSharper disable SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault + +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using KurrentDB.Client.Core.Internal.Exceptions; +using static KurrentDB.Client.Constants; + +namespace KurrentDB.Client; + +public class MetadataJsonConverter : JsonConverter> { + public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + var metadata = new Dictionary(); + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { + if (reader.GetString() is not { } propertyName || string.IsNullOrWhiteSpace(propertyName)) + throw new JsonException("Property name cannot be empty or whitespace"); + + reader.Read(); + + var value = reader.TokenType switch { + JsonTokenType.None => null, + JsonTokenType.Null => null, + JsonTokenType.True => true, + JsonTokenType.False => false, + JsonTokenType.String => ParseString(reader, propertyName), + JsonTokenType.Number => ParseNumber(reader), + _ => throw new JsonException($"Unsupported metadata value type ({reader.TokenType}) for property '{propertyName}'") + }; + + metadata[propertyName] = value; + } + + return metadata; + + static object ParseNumber(Utf8JsonReader reader) { + if (reader.TryGetInt32(out var intValue)) + return intValue; + + if (reader.TryGetInt64(out var longValue)) + return longValue; + + if (reader.TryGetDouble(out var doubleValue)) + return doubleValue; + +#if NET48 + return Encoding.UTF8.GetString(reader.ValueSpan.ToArray()); +#else + return Encoding.UTF8.GetString(reader.ValueSpan); +#endif + } + + static object? ParseString(Utf8JsonReader reader, string propertyName) { + if (propertyName.Equals(Metadata.SchemaName, StringComparison.OrdinalIgnoreCase)) { + var value = reader.GetString(); + return string.IsNullOrWhiteSpace(value) ? "" : value; + } + + if (propertyName.Equals(Metadata.SchemaDataFormat, StringComparison.OrdinalIgnoreCase)) + return Enum.TryParse(reader.GetString(), ignoreCase: true, out var format) + ? format + : SchemaDataFormat.Unspecified; + + if (reader.TryGetGuid(out var guid)) return guid; + if (reader.TryGetDateTime(out var dateTime)) return dateTime; + if (reader.TryGetTimeSpan(out var timeSpan)) return timeSpan; + if (reader.TryGetBytesFromBase64(out var bytes)) return new ReadOnlyMemory(bytes); + + return reader.GetString(); + } + } + + public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, value, options); +} diff --git a/src/KurrentDB.Client/Streams/Streams/Model/SchemaDataFormat.cs b/src/KurrentDB.Client/Streams/Streams/Model/SchemaDataFormat.cs new file mode 100644 index 000000000..abf4ed31d --- /dev/null +++ b/src/KurrentDB.Client/Streams/Streams/Model/SchemaDataFormat.cs @@ -0,0 +1,19 @@ +namespace KurrentDB.Client; + +#pragma warning disable CS8509 +public enum SchemaDataFormat { + /// + /// The data format is not specified. + /// + Unspecified = 0, + + /// + /// The data is in JSON format. + /// + Json = 1, + + /// + /// The data is in raw bytes format. + /// + Bytes = 4 +} diff --git a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs new file mode 100644 index 000000000..d40a80b6f --- /dev/null +++ b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs @@ -0,0 +1,79 @@ +#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). + +using Google.Protobuf; +using Google.Protobuf.Collections; +using KurrentDB.Client.Schema.Serialization.Json; +using KurrentDB.Protocol.Streams.V2; + +namespace KurrentDB.Client; + +static class StreamsClientMapper { + internal static JsonSerializer JsonSerializer { get; } = new(); + + public static async IAsyncEnumerable Map(this IEnumerable source) { + foreach (var message in source) + yield return await message + .Map() + .ConfigureAwait(false); + } + + public static ValueTask Map(this EventData source) { + Dictionary metadata; + + if (source.Metadata.IsEmpty) { + metadata = new(); + } else { + try { + metadata = JsonSerializer.Deserialize>(source.Metadata) ?? new(); + } catch (Exception ex) { + throw new ArgumentException( + $"Event metadata must be valid JSON that can be deserialized to Dictionary. This limitation will be removed in the next major release" + + + $"Deserialization failed: {ex.Message}", + nameof(source), + ex + ); + } + } + + metadata[Constants.Metadata.SchemaName] = source.Type; + metadata[Constants.Metadata.SchemaDataFormat] = source.ContentType is Constants.Metadata.ContentTypes.ApplicationJson + ? SchemaDataFormat.Json + : SchemaDataFormat.Bytes; + + var record = new AppendRecord { + RecordId = source.EventId.ToString(), + Data = ByteString.CopyFrom(source.Data.Span), + Properties = { metadata.MapToDynamicMapField() } + }; + + return new ValueTask(record); + } + + public static Exception Map(this AppendStreamFailure source) { + return source.ErrorCase switch { + AppendStreamFailure.ErrorOneofCase.StreamRevisionConflict => new WrongExpectedVersionException( + source.Stream, + StreamState.StreamRevision((ulong)source.StreamRevisionConflict.StreamRevision) + ), + AppendStreamFailure.ErrorOneofCase.AccessDenied => new AccessDeniedException(), + AppendStreamFailure.ErrorOneofCase.StreamDeleted => new StreamDeletedException(source.Stream), + AppendStreamFailure.ErrorOneofCase.TransactionMaxSizeExceeded => new TransactionMaxSizeExceededException(source.TransactionMaxSizeExceeded.MaxSize), + }; + } + + public static AppendStreamSuccess Map(this Protocol.Streams.V2.AppendStreamSuccess source) => + new(source.Stream, source.Position); + + public static AppendStreamFailures Map(this RepeatedField source) => + new(source.Select(failure => failure.Map())); + + public static AppendStreamSuccesses Map(this RepeatedField source) => + new(source.Select(success => success.Map())); + + public static AppendStreamFailures Map(this MultiStreamAppendResponse.Types.Failure source) => + new(source.Output.Map()); + + public static AppendStreamSuccesses Map(this MultiStreamAppendResponse.Types.Success source) => + new(source.Output.Map()); +} diff --git a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientModel.cs b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientModel.cs new file mode 100644 index 000000000..3e59a01ad --- /dev/null +++ b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientModel.cs @@ -0,0 +1,74 @@ +using JetBrains.Annotations; + +namespace KurrentDB.Client; + +/// +/// Represents a request to append events to a specific stream in KurrentDB. +/// +/// +/// The name of the stream to which the events are to be appended. +/// +/// +/// The expected state of the stream before performing the append operation +/// to enforce optimistic concurrency control by ensuring that the +/// stream's actual state matches the expected state before appending. +/// +/// +/// A collection of representing the events being appended +/// to the stream. Each event can contain a payload and an associated metadata. +/// +[PublicAPI] +public record AppendStreamRequest(string Stream, StreamState ExpectedState, IEnumerable Messages); + +/// +/// Represents the successful outcome of an append operation to a specific stream in the system. +/// +/// +/// The name of the stream where the events have been successfully appended. +/// +/// +/// The position in the stream after the append operation, indicating where the event(s) were written. +/// +[PublicAPI] +public record AppendStreamSuccess(string Stream, long Position); + +/// +/// Represents the result of a multi-stream append operation in KurrentDB. +/// +/// +/// +[PublicAPI] +public abstract class MultiAppendWriteResult { + public abstract bool IsSuccess { get; } + public bool IsFailure => !IsSuccess; +} + +[PublicAPI] +public sealed class MultiAppendSuccess : MultiAppendWriteResult { + public override bool IsSuccess => true; + public AppendStreamSuccesses Successes { get; } + + internal MultiAppendSuccess(AppendStreamSuccesses successes) => + Successes = successes; +} + +[PublicAPI] +public sealed class MultiAppendFailure : MultiAppendWriteResult { + public override bool IsSuccess => false; + public AppendStreamFailures Failures { get; } + + internal MultiAppendFailure(AppendStreamFailures failures) => + Failures = failures; +} + +[PublicAPI] +public class AppendStreamSuccesses : List { + public AppendStreamSuccesses() { } + public AppendStreamSuccesses(IEnumerable input) : base(input) { } +} + +[PublicAPI] +public class AppendStreamFailures : List { + public AppendStreamFailures() { } + public AppendStreamFailures(IEnumerable input) : base(input) { } +} diff --git a/test/KurrentDB.Client.Tests.Common/Facts/MinimumVersion.cs b/test/KurrentDB.Client.Tests.Common/Facts/MinimumVersion.cs new file mode 100644 index 000000000..83ebcbebb --- /dev/null +++ b/test/KurrentDB.Client.Tests.Common/Facts/MinimumVersion.cs @@ -0,0 +1,112 @@ +// ReSharper disable InconsistentNaming + +using KurrentDB.Client.Tests.FluentDocker; + +namespace KurrentDB.Client.Tests; + +[PublicAPI] +public class MinimumVersion { + public class FactAttribute : Xunit.FactAttribute { + readonly int Major; + readonly int? Minor; + readonly int? Patch; + + public FactAttribute(int major) { + Major = major; + } + + public FactAttribute(int major, int minor) { + Major = major; + Minor = minor; + } + + public FactAttribute(int major, int minor, int patch) { + Major = major; + Minor = minor; + Patch = patch; + } + + public override string? Skip { + get { + var currentVersion = TestContainerService.Version; + var requiredVersionString = Patch.HasValue + ? $"{Major}.{Minor}.{Patch}" + : Minor.HasValue + ? $"{Major}.{Minor}" + : $"{Major}"; + + if (Patch.HasValue) { + var required = new Version(Major, Minor!.Value, Patch.Value); + return currentVersion < required + ? $"Test requires minimum version {requiredVersionString}, but current version is {currentVersion}" + : null; + } + + if (Minor.HasValue) { + if (currentVersion.Major < Major || + (currentVersion.Major == Major && currentVersion.Minor < Minor.Value)) { + return $"Test requires minimum version {requiredVersionString}, but current version is {currentVersion}"; + } + } else { + if (currentVersion.Major < Major) + return $"Test requires minimum major version {requiredVersionString}, but current version is {currentVersion}"; + } + + return null; + } + set => throw new NotSupportedException(); + } + } + + public class TheoryAttribute : Xunit.TheoryAttribute { + readonly int Major; + readonly int? Minor; + readonly int? Patch; + + public TheoryAttribute(int major) { + Major = major; + } + + public TheoryAttribute(int major, int minor) { + Major = major; + Minor = minor; + } + + public TheoryAttribute(int major, int minor, int patch) { + Major = major; + Minor = minor; + Patch = patch; + } + + public override string? Skip { + get { + var currentVersion = TestContainerService.Version; + var requiredVersionString = Patch.HasValue + ? $"{Major}.{Minor}.{Patch}" + : Minor.HasValue + ? $"{Major}.{Minor}" + : $"{Major}"; + + if (Patch.HasValue) { + var required = new Version(Major, Minor!.Value, Patch.Value); + return currentVersion < required + ? $"Test requires minimum version {requiredVersionString}, but current version is {currentVersion}" + : null; + } + + if (Minor.HasValue) { + if (currentVersion.Major < Major || + (currentVersion.Major == Major && currentVersion.Minor < Minor.Value)) { + return $"Test requires minimum version {requiredVersionString}, but current version is {currentVersion}"; + } + } else { + if (currentVersion.Major < Major) + return $"Test requires minimum major version {requiredVersionString}, but current version is {currentVersion}"; + } + + return null; + } + set => throw new NotSupportedException(); + } + } +} diff --git a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs new file mode 100644 index 000000000..afbf72fe6 --- /dev/null +++ b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs @@ -0,0 +1,126 @@ +using Humanizer; +using static KurrentDB.Client.Constants; +using JsonSerializer = KurrentDB.Client.Schema.Serialization.Json.JsonSerializer; + +namespace KurrentDB.Client.Tests.Streams; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:MultiStreamAppend")] +public class MultiStreamAppendTests(ITestOutputHelper output, KurrentDBPermanentFixture fixture) + : KurrentDBPermanentTests(output, fixture) { + static JsonSerializer JsonSerializer { get; } = new(); + + [MinimumVersion.Fact(25, 1)] + public async Task append_events_with_invalid_metadata_format_throws_exceptions() { + // Arrange + var stream = Fixture.GetStreamName(); + + var invalidMetadata = "invalid"u8.ToArray(); + + AppendStreamRequest[] requests = [ + new(stream, StreamState.NoStream, Fixture.CreateTestEvents(3, metadata: invalidMetadata)), + ]; + + // Act & Assert + var exception = await Fixture.Streams + .MultiStreamAppendAsync(requests.ToAsyncEnumerable()).AsTask() + .ShouldThrowAsync(); + + Assert.Contains("Deserialization failed:", exception.Message); + } + + [MinimumVersion.Fact(25, 1)] + public async Task append_events_to_multiple_streams() { + // Arrange + var stream1 = Fixture.GetStreamName(); + var stream2 = Fixture.GetStreamName(); + + var expectedMetadata = new TestMetadata { + StringValue = "Foo", + BooleanValue = true, + Int32Value = 42, + Int64Value = 9223372036854775807L, + DoubleValue = 2.718281828, + DateTimeValue = DateTime.Now, + TimeSpanValue = 2.5.Hours(), + ByteArrayValue = "Bar"u8.ToArray() + }; + + var metadataBytes = JsonSerializer.Serialize(expectedMetadata); + + AppendStreamRequest[] requests = [ + new(stream1, StreamState.NoStream, Fixture.CreateTestEvents(3, metadata: metadataBytes).ToArray()), + new(stream2, StreamState.NoStream, Fixture.CreateTestEvents(2, metadata: metadataBytes).ToArray()) + ]; + + // Act + var result = await Fixture.Streams.MultiStreamAppendAsync(requests.ToAsyncEnumerable()); + + // Assert + result.IsSuccess.ShouldBeTrue(); + var success = result.ShouldBeOfType(); + success.Successes.Count.ShouldBe(2); + + var stream1Events = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream1, StreamPosition.Start, 10) + .ToArrayAsync(); + + var stream2Events = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream2, StreamPosition.Start, 10) + .ToArrayAsync(); + + var metadata = JsonSerializer.Deserialize>(stream1Events.First().OriginalEvent.Metadata); + + stream1Events.Length.ShouldBe(3); + stream2Events.Length.ShouldBe(2); + + metadata.ShouldNotBeNull(); + metadata[Metadata.SchemaName].ShouldBe("test-event-type"); + metadata[Metadata.SchemaDataFormat].ShouldBe(SchemaDataFormat.Json); + metadata["stringValue"].ShouldBe(expectedMetadata.StringValue); + metadata["booleanValue"].ShouldBe(expectedMetadata.BooleanValue); + metadata["int32Value"].ShouldBe(expectedMetadata.Int32Value); + metadata["int64Value"].ShouldBe(expectedMetadata.Int64Value); + metadata["doubleValue"].ShouldBe(expectedMetadata.DoubleValue); + metadata["dateTimeValue"].ShouldBe(expectedMetadata.DateTimeValue); + metadata["timeSpanValue"].ShouldBe(expectedMetadata.TimeSpanValue); + metadata["byteArrayValue"].ShouldBe(expectedMetadata.ByteArrayValue); + } + + [MinimumVersion.Fact(25, 1)] + public async Task appending_events_with_failures() { + // Arrange + var stream1 = Fixture.GetStreamName(); + var stream2 = Fixture.GetStreamName(); + var stream3 = Fixture.GetStreamName(); + + AppendStreamRequest[] requests = [ + new(stream1, StreamState.StreamExists, Fixture.CreateTestEvents(3).ToArray()), // does not exist + new(stream2, StreamState.NoStream, Fixture.CreateTestEvents(2).ToArray()), + new(stream3, StreamState.StreamExists, Fixture.CreateTestEvents(3).ToArray()), // does not exist + ]; + + // Act + var result = await Fixture.Streams.MultiStreamAppendAsync(requests.ToAsyncEnumerable()); + + // Assert + result.IsFailure.ShouldBeTrue(); + var failure = result.ShouldBeOfType(); + failure.Failures.Count.ShouldBe(2); + + failure.Failures + .Select(f => f.ShouldBeOfType()) + .Count().ShouldBe(2); + } +} + +public class TestMetadata { + public string? StringValue { get; init; } + public bool BooleanValue { get; init; } + public int Int32Value { get; init; } + public long Int64Value { get; init; } + public double DoubleValue { get; init; } + public DateTime DateTimeValue { get; init; } + public TimeSpan TimeSpanValue { get; init; } + public byte[]? ByteArrayValue { get; init; } +} From a6f3199851de1d7517e848c463249909eacaabae Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 13 Aug 2025 10:58:07 +0400 Subject: [PATCH 08/29] Document multi stream append --- docs/api/appending-events.md | 115 ++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/docs/api/appending-events.md b/docs/api/appending-events.md index 35f220249..10d047649 100644 --- a/docs/api/appending-events.md +++ b/docs/api/appending-events.md @@ -136,4 +136,117 @@ await client.AppendToStreamAsync( new[] { eventData }, userCredentials: new UserCredentials("admin", "changeit") ); -``` \ No newline at end of file +``` + +## Append to multiple streams + +::: note +This feature is only available in KurrentDB 25.1 and later. +::: + +You can append events to multiple streams in a single atomic operation. Either all streams are updated, or the entire operation fails. + +The `MultiStreamAppendAsync` method accepts a collection of `AppendStreamRequest` objects and returns a `MultiAppendWriteResult`. Each `AppendStreamRequest` contains: + +- **Stream** - The name of the stream +- **ExpectedState** - The expected state of the stream for optimistic concurrency control +- **Messages** - A collection of `EventData` objects to append + +The operation returns either: +- `MultiAppendSuccess` - Successful append results for all streams +- `MultiAppendFailure` - Specific exceptions for any failed operations + +::: warning +Event metadata in `EventData` must be valid JSON deserializable to +`Dictionary`. This requirement will be removed in a future +major release. +::: + +Here's a basic example of appending events to multiple streams: + +```cs +using System.Text.Json; + +var metadata = JsonSerializer.SerializeToUtf8Bytes( + new { + Timestamp = DateTime.UtcNow, + Source = "OrderProcessingSystem", + Version = 1.0 + } +); + +AppendStreamRequest[] requests = [ + new( + "order-stream-1", + StreamState.Any, + [ + new EventData( + Uuid.NewUuid(), + "OrderCreated", + JsonSerializer.SerializeToUtf8Bytes( + new { + OrderId = "12345", + Amount = 99.99 + } + ), + metadata + ) + ] + ), + new( + "inventory-stream-1", + StreamState.Any, + [ + new EventData( + Uuid.NewUuid(), + "ItemReserved", + JsonSerializer.SerializeToUtf8Bytes( + new { + ItemId = "ABC123", + Quantity = 2 + } + ), + metadata + ) + ] + ) +]; + +var result = await client.MultiStreamAppendAsync(requests.ToAsyncEnumerable()); + +if (result is MultiAppendSuccess { Successes: var successes }) + foreach (var item in successes) + Console.WriteLine($"Stream '{item.Stream}' updated at position {item.Position}"); +``` + +If the operation doesn't succeed, it can fail with the following exceptions: + +```cs +var result = await client.MultiStreamAppendAsync(requests.ToAsyncEnumerable()); + +if (result is MultiAppendFailure { Failures: var failures }) { + foreach (var error in failures) { + switch (error) { + case WrongExpectedVersionException ex: + Console.WriteLine($"Version conflict in stream: {ex.Message}"); + break; + + case AccessDeniedException: + Console.WriteLine("Access denied to one or more streams"); + break; + + case StreamDeletedException ex: + Console.WriteLine($"Stream was deleted: {ex.Message}"); + break; + + case TransactionMaxSizeExceededException ex: + Console.WriteLine($"Transaction too large: {ex.Message}"); + break; + + default: + Console.WriteLine($"Unexpected error: {error.Message}"); + break; + } + } +} +``` From 9cd68fecc9763280e72312af824c3d51e3bbd2b9 Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 13 Aug 2025 11:59:55 +0400 Subject: [PATCH 09/29] Improve error message --- .../Streams/Streams/Model/Metadata/MetadataJsonConverter.cs | 3 ++- .../Streams/Streams/Model/StreamsClientMapper.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataJsonConverter.cs b/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataJsonConverter.cs index 8448b019c..ee35d9c84 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataJsonConverter.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataJsonConverter.cs @@ -1,5 +1,6 @@ // ReSharper disable SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault +using System.Buffers; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -44,7 +45,7 @@ static object ParseNumber(Utf8JsonReader reader) { return doubleValue; #if NET48 - return Encoding.UTF8.GetString(reader.ValueSpan.ToArray()); + return Encoding.UTF8.GetString(reader.ValueSequence.ToArray()); #else return Encoding.UTF8.GetString(reader.ValueSpan); #endif diff --git a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs index d40a80b6f..8a06855b9 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs @@ -27,8 +27,8 @@ public static ValueTask Map(this EventData source) { metadata = JsonSerializer.Deserialize>(source.Metadata) ?? new(); } catch (Exception ex) { throw new ArgumentException( - $"Event metadata must be valid JSON that can be deserialized to Dictionary. This limitation will be removed in the next major release" - + + $"Event metadata must be valid JSON with property values limited to: null, boolean, number, string, Guid, DateTime, TimeSpan, or Base64-encoded byte arrays. " + + $"Complex objects and arrays are not supported. This limitation will be removed in the next major release. " + $"Deserialization failed: {ex.Message}", nameof(source), ex From 13fa68dd8dd5e64b136bc0b6edc5de6dd3601194 Mon Sep 17 00:00:00 2001 From: William Chong Date: Fri, 15 Aug 2025 15:08:09 +0400 Subject: [PATCH 10/29] Add extension methods --- docs/api/appending-events.md | 2 +- .../Streams/MultiStreamAppend.Extensions.cs | 37 +++++++++++++++++++ .../Streams/MultiStreamAppendTests.cs | 6 +-- 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 test/KurrentDB.Client.Tests/Streams/MultiStreamAppend.Extensions.cs diff --git a/docs/api/appending-events.md b/docs/api/appending-events.md index 10d047649..41d030ffb 100644 --- a/docs/api/appending-events.md +++ b/docs/api/appending-events.md @@ -212,7 +212,7 @@ AppendStreamRequest[] requests = [ ) ]; -var result = await client.MultiStreamAppendAsync(requests.ToAsyncEnumerable()); +var result = await client.MultiStreamAppendAsync(requests); if (result is MultiAppendSuccess { Successes: var successes }) foreach (var item in successes) diff --git a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppend.Extensions.cs b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppend.Extensions.cs new file mode 100644 index 000000000..b8aa692e5 --- /dev/null +++ b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppend.Extensions.cs @@ -0,0 +1,37 @@ +namespace KurrentDB.Client.Tests.Streams; + +public static class MultiStreamAppendExtensions { + public static ValueTask MultiStreamAppendAsync( + this KurrentDBClient client, IEnumerable requests, CancellationToken cancellationToken = default + ) => + client.MultiStreamAppendAsync(requests.ToAsyncEnumerable(), cancellationToken); + + public static ValueTask MultiStreamAppendAsync( + this KurrentDBClient client, AppendStreamRequest request, CancellationToken cancellationToken = default + ) => + client.MultiStreamAppendAsync([request], cancellationToken); + + /// + /// Appends a series of messages to a specified stream while specifying the expected stream state. + /// + /// The name of the stream to which the messages will be appended. + /// The expected state of the stream to ensure consistency during the append operation. + /// A collection of messages to be appended to the stream. + /// A token to observe while waiting for the operation to complete, allowing for cancellation if needed. + public static ValueTask MultiStreamAppendAsync( + this KurrentDBClient client, string stream, StreamState expectedState, IEnumerable messages, + CancellationToken cancellationToken + ) => + client.MultiStreamAppendAsync(new AppendStreamRequest(stream, expectedState, messages), cancellationToken); + + public static ValueTask MultiStreamAppendAsync( + this KurrentDBClient client, string stream, StreamState expectedState, EventData message, + CancellationToken cancellationToken + ) => + client.MultiStreamAppendAsync(new AppendStreamRequest(stream, expectedState, [message]), cancellationToken); + + public static ValueTask MultiStreamAppendAsync( + this KurrentDBClient client, string stream, IEnumerable messages, CancellationToken cancellationToken + ) => + client.MultiStreamAppendAsync(new AppendStreamRequest(stream, StreamState.Any, messages), cancellationToken); +} diff --git a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs index afbf72fe6..371d638ea 100644 --- a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs +++ b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs @@ -23,7 +23,7 @@ public async Task append_events_with_invalid_metadata_format_throws_exceptions() // Act & Assert var exception = await Fixture.Streams - .MultiStreamAppendAsync(requests.ToAsyncEnumerable()).AsTask() + .MultiStreamAppendAsync(requests).AsTask() .ShouldThrowAsync(); Assert.Contains("Deserialization failed:", exception.Message); @@ -54,7 +54,7 @@ public async Task append_events_to_multiple_streams() { ]; // Act - var result = await Fixture.Streams.MultiStreamAppendAsync(requests.ToAsyncEnumerable()); + var result = await Fixture.Streams.MultiStreamAppendAsync(requests); // Assert result.IsSuccess.ShouldBeTrue(); @@ -101,7 +101,7 @@ public async Task appending_events_with_failures() { ]; // Act - var result = await Fixture.Streams.MultiStreamAppendAsync(requests.ToAsyncEnumerable()); + var result = await Fixture.Streams.MultiStreamAppendAsync(requests); // Assert result.IsFailure.ShouldBeTrue(); From 045003d58032714e25ed314ba5589794186591f0 Mon Sep 17 00:00:00 2001 From: William Chong Date: Mon, 18 Aug 2025 12:50:23 +0400 Subject: [PATCH 11/29] Remove unused proto files --- .../protocol/v2/streams/streams_manage.proto | 123 ----------- .../protocol/v2/streams/streams_read.proto | 200 ------------------ 2 files changed, 323 deletions(-) delete mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_manage.proto delete mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_read.proto diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_manage.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_manage.proto deleted file mode 100644 index 639439794..000000000 --- a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_manage.proto +++ /dev/null @@ -1,123 +0,0 @@ -syntax = "proto3"; - -package kurrentdb.protocol.v2; - -option csharp_namespace = "KurrentDB.Protocol.Streams.V2"; -option java_package = "io.kurrentdb.protocol.streams.v2"; -option java_multiple_files = true; - -import "google/protobuf/timestamp.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/descriptor.proto"; - -import "streams/error_details.proto"; -import "core.proto"; - -service StreamsManagementService { - rpc DeleteStream(DeleteStreamRequest) returns (DeleteStreamResponse); - - rpc GetStreamInfo(DeleteStreamRequest) returns (DeleteStreamResponse); -} - -message DeleteStreamRequest { - // The name of the stream. - string stream = 1; - // The expected revision of the stream. If the stream's current revision does - // not match, the operation will fail. - // The expected revision can also be one of the special values - // from ExpectedRevisionConstants. - // Missing value means no expectation, the same as EXPECTED_REVISION_CONSTANTS_ANY - optional sint64 expected_revision = 3; - - // If true, the stream will be tombstoned instead of deleted. - // A tombstoned stream cannot be used anymore. - bool tombstone = 4; -} - -message DeleteStreamSuccess { - // The position of the last appended record in the stream. - int64 position = 2; - // The expected revision of the stream after the append operation. - int64 stream_revision = 3; -} - -message DeleteStreamFailure { - // The error details - oneof error { - // Failed because the actual stream revision didn't match the expected revision. - StreamsErrorDetails.StreamRevisionConflict stream_revision_conflict = 2; - // Failed because the client lacks sufficient permissions. - CoreErrorDetails.AccessDenied access_denied = 3; - // Failed because the target stream has been deleted. - StreamsErrorDetails.StreamDeleted stream_deleted = 4; - // Failed because the stream has been tombstoned. - StreamsErrorDetails.StreamTombstoned stream_tombstoned = 5; - // Failed because the stream was not found. - StreamsErrorDetails.StreamNotFound stream_not_found = 6; - } -} - -message DeleteStreamResponse { - // The result of the operation. - oneof result { - // Success represents the successful outcome of a delete operation. - DeleteStreamSuccess success = 1; - // Failure represents the details of a failed delete operation. - DeleteStreamFailure failure = 2; - } -} - -// Represents the access control list (ACL) for a stream. -// The ACL defines who can read, write, delete, or manage the stream. -message StreamAcl { - // Roles and users permitted to read the stream - repeated string read_roles = 1; - // Roles and users permitted to write to the stream - repeated string write_roles = 2; - // Roles and users permitted to delete the stream - repeated string delete_roles = 3; - // Roles and users permitted to read stream metadata - repeated string meta_read_roles = 4; - // Roles and users permitted to write stream metadata - repeated string meta_write_roles = 5; -} - -message StreamMetadata { - // Maximum age of events allowed in the stream. - optional google.protobuf.Duration max_age = 1; - // Stream revision from which previous events can be scavenged. - optional int64 TruncateBefore = 2; - // Amount of time for which the stream head is cacheable. - optional google.protobuf.Duration cache_control = 4; - // Maximum number of events allowed in the stream. - optional int32 MaxCount = 5; - // User-provided metadata. - optional string custom_metadata = 6; - // Access control list for the stream. - StreamAcl acl = 7; -} - -// Represents the state of a stream. -// The state is used to determine how the stream should be handled -// during operations such as deletion or retrieval. -enum StreamState { - NotFound = 0; - Active = 1; - Deleted = 2; - Tombstoned = 3; -} - -// Represents the information about a stream. -message StreamInfo { - // The last stream revision of the stream. - int64 last_revision = 1; - // The last position in the stream. - int64 last_position = 2; - // The state of the stream. - StreamState State = 5; - // The metadata associated with the stream. - StreamMetadata metadata = 4; - // The revision of the stream metadata. - // This is the revision of the metadata, not the stream itself. - int64 metadata_revision = 3; -} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_read.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_read.proto deleted file mode 100644 index 77b567739..000000000 --- a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_read.proto +++ /dev/null @@ -1,200 +0,0 @@ -syntax = "proto3"; - -package kurrentdb.protocol.v2; - -option csharp_namespace = "KurrentDB.Protocol.Streams.V2"; -option java_package = "io.kurrentdb.protocol.streams.v2"; -option java_multiple_files = true; - -import "google/protobuf/timestamp.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/descriptor.proto"; - -import "streams/error_details.proto"; -import "core.proto"; - -service StreamsReadService { - // Retrieve batches of records continuously. - rpc ReadSession(ReadRequest) returns (stream ReadResponse); - - // Retrieve batches of records continuously. - rpc Read(ReadRequest) returns (stream ReadResponse); -} - -//=================================================================== -// Read Operations -//=================================================================== - -// The scope of the read filter determines where the filter will be applied. -enum ReadFilterScope { - READ_FILTER_SCOPE_UNSPECIFIED = 0; - // The filter will be applied to the record stream name - READ_FILTER_SCOPE_STREAM = 1; - // The filter will be applied to the record schema name - READ_FILTER_SCOPE_SCHEMA_NAME = 2; - // The filter will be applied to the properties of the record - READ_FILTER_SCOPE_PROPERTIES = 3; - // The filter will be applied to all the record properties - // including the stream and schema name - READ_FILTER_SCOPE_RECORD = 4; - // The filter will be applied to the index name - READ_FILTER_SCOPE_INDEX_NAME = 5; -} - -// The filter to apply when reading records from the database -// The combination of stream scope and literal expression indicates a direct stream name match, -// while a regex expression indicates a pattern match across multiple streams. -message ReadFilter { - // The scope of the filter. - ReadFilterScope scope = 1; - // The expression can be a regular expression or a literal value. - // If it starts with "~" it will be considered a regex. - string expression = 2; -} - -// Record retrieved from the database. -message Record { - // The unique identifier of the record in the database. - string record_id = 1; - // The position of the record in the database. - int64 position = 5; - // The actual data payload of the record, stored as bytes. - bytes data = 2; - // Additional information about the record. - map properties = 3; - // When the record was created. - google.protobuf.Timestamp timestamp = 4; - // The stream to which the record belongs. - optional string stream = 6; - // The revision of the stream created when the record was appended. - optional int64 stream_revision = 7; -} - -// The direction in which to read records from the database (forwards or backwards). -enum ReadDirection { - READ_DIRECTION_FORWARDS = 0; - READ_DIRECTION_BACKWARDS = 1; -} - -// Initial position at which the cursor will be set when subscribing. -enum ReadInitialPosition { - READ_INITIAL_POSITION_LATEST = 0; - READ_INITIAL_POSITION_EARLIEST = 1; -} - -// Limit how many records can be retrieved. -message ReadLimit { - // The maximum number of records to return. - // If not specified, an active read session will be created - // and the client will receive records as they are appended to the stream. - optional uint64 count = 1; -} - -// Represents the successful outcome of a read operation. -message ReadSuccess { - repeated Record records = 1; -} - -// Represents the detailed error information when a read operation fails. -message ReadFailure { - // The error details - oneof error { - // Failed because the client lacks sufficient permissions. - CoreErrorDetails.AccessDenied access_denied = 1; - // Failed because the target stream has been deleted. - StreamsErrorDetails.StreamDeleted stream_deleted = 2; - // Failed because the stream has been tombstoned. - StreamsErrorDetails.StreamTombstoned stream_tombstoned = 4; - // Failed because the expected stream revision did not match the actual revision. - StreamsErrorDetails.StreamNotFound stream_not_found = 3; - } -} - -// Read request message that contains the filter, starting position, limit, -// direction, and heartbeat options for reading records from the database. -// It allows the client to specify how to read records, including the filter to apply, -// the starting position, the maximum number of records to return, the direction of reading, -// and whether to enable heartbeats for monitoring session health. -// The batch size can also be specified to control how many records are read in a single batch. -message ReadRequest { - // The filter to apply when reading records. - optional ReadFilter filter = 1; - // The starting position of the log from which to read records. - optional int64 start_position = 2; - - // Position at which the cursor will be set when subscribing. - // Used when no start position is specified. - // If not specified, the default is READ_INITIAL_POSITION_LATEST. - ReadInitialPosition initial_position = 7; - - // Limit how many records can be returned. - // This will get capped at the default limit, - // which is up to 1000 records. - optional int64 limit = 3; - // The direction in which to read the stream (forwards or backwards). - ReadDirection direction = 4; - // Heartbeats can be enabled to monitor end-to-end session health. - HeartbeatOptions heartbeats = 5; - // The number of records to read in a single batch. - int32 batch_size = 6; -} - -// Read response message that can contain either a successful read result, -// a failure result, or a heartbeat message. -// This allows the client to handle different outcomes of the read operation. -message ReadResponse { - oneof result { - // Success represents the successful outcome of an read operation. - ReadSuccess success = 1; - // Failure represents the details of a failed read operation. - ReadFailure failure = 2; - // Heartbeat represents the health check of the read operation when - // the server has not found any records matching the filter for the specified - // period of time or records threshold. - // A heartbeat will be sent when the initial switch to real-time tailing happens. - Heartbeat heartbeat = 3; - } -} - -// A health check will be sent when the server has not found any records -// matching the filter for the specified period of time or records threshold. A -// heartbeat will be sent when the initial switch to real-time tailing happens. -message HeartbeatOptions { - // Enable heartbeats for monitoring end-to-end session health. - // If enabled, heartbeats will be sent periodically or when the records threshold is reached. - // If disabled, no heartbeats will be sent. - bool enable = 1; - // The period after which a heartbeat will be sent if no records are found. - // If not specified, the default period is 30 seconds. - // This is the maximum time to wait before sending a heartbeat. - // If the period is set to 0, heartbeats will not be sent based on time. - // If the period is set to a non-zero value, heartbeats will be sent - optional google.protobuf.Duration period = 2; // 30 seconds - optional int32 records_threshold = 3; // 500 -} - -// The type of heartbeat sent by the server to indicate the status of the read operation. -// It can indicate whether the subscription is caught up, fell behind, or -// the filter has not been satisfied after a period of time or records threshold. -enum HeartbeatType { - HEARTBEAT_TYPE_UNSPECIFIED = 0; - HEARTBEAT_TYPE_CHECKPOINT = 1; - HEARTBEAT_TYPE_CAUGHT_UP = 2; - HEARTBEAT_TYPE_FELL_BEHIND = 3; -} - -// Heartbeat message sent by the server to indicate the status of the read operation. -// It contains the type of heartbeat, the position for resuming reads, -// and the timestamp when the heartbeat was sent. -message Heartbeat { - // This indicates whether the subscription is caught up, fell behind, or - // the filter has not been satisfied after a period of time or records threshold. - HeartbeatType type = 1; - // Checkpoint for resuming reads. - // It will always be populated unless the database is empty. - int64 position = 2; - // When the heartbeat was sent. - google.protobuf.Timestamp timestamp = 3; -} - -//=================================================================== From 82c2b9f7349bffbcbe760ba4af644bc4efdee173 Mon Sep 17 00:00:00 2001 From: William Chong Date: Mon, 18 Aug 2025 16:29:29 +0400 Subject: [PATCH 12/29] Remove JsonSerializer --- .../Json/JsonSchemaSerializerOptions.cs | 27 ---------------- .../Serialization/Json/JsonSerializer.cs | 31 ------------------- .../Streams/Model/StreamsClientMapper.cs | 22 +++++++++++-- .../Streams/MultiStreamAppendTests.cs | 7 ++--- 4 files changed, 22 insertions(+), 65 deletions(-) delete mode 100644 src/KurrentDB.Client/Schema/Serialization/Json/JsonSchemaSerializerOptions.cs delete mode 100644 src/KurrentDB.Client/Schema/Serialization/Json/JsonSerializer.cs diff --git a/src/KurrentDB.Client/Schema/Serialization/Json/JsonSchemaSerializerOptions.cs b/src/KurrentDB.Client/Schema/Serialization/Json/JsonSchemaSerializerOptions.cs deleted file mode 100644 index 62e454eae..000000000 --- a/src/KurrentDB.Client/Schema/Serialization/Json/JsonSchemaSerializerOptions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using JetBrains.Annotations; - -namespace KurrentDB.Client.Schema.Serialization.Json; - -[PublicAPI] -public record JsonSchemaSerializerOptions { - public static readonly JsonSchemaSerializerOptions Default = new(); - - public static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new(JsonSerializerOptions.Default) { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = false, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, - UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - Converters = { - new MetadataJsonConverter(), - new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) - } - }; - - public JsonSerializerOptions JsonSerializerOptions { get; init; } = DefaultJsonSerializerOptions; - public bool UseProtobufFormatter { get; init; } = true; -} diff --git a/src/KurrentDB.Client/Schema/Serialization/Json/JsonSerializer.cs b/src/KurrentDB.Client/Schema/Serialization/Json/JsonSerializer.cs deleted file mode 100644 index 215ce1fb4..000000000 --- a/src/KurrentDB.Client/Schema/Serialization/Json/JsonSerializer.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using KurrentDB.Client.Core.Internal; - -namespace KurrentDB.Client.Schema.Serialization.Json; - -/// -/// A serializer class that supports serialization and deserialization of objects -/// -public class JsonSerializer(JsonSerializerOptions? options = null) { - JsonSerializerOptions Options { get; } = options ?? JsonSchemaSerializerOptions.DefaultJsonSerializerOptions; - - public ReadOnlyMemory Serialize(object? value) => - System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value, Options); - - public object? Deserialize(ReadOnlyMemory data, Type type) => - !type.IsMissing() - ? System.Text.Json.JsonSerializer.Deserialize(data.Span, type, Options) - : System.Text.Json.JsonSerializer.Deserialize(data.Span, Options); - - public async ValueTask Deserialize(Stream data, Type type, CancellationToken cancellationToken = default) { - var value = !type.IsMissing() - ? await System.Text.Json.JsonSerializer.DeserializeAsync(data, type, Options, cancellationToken).ConfigureAwait(false) - : await System.Text.Json.JsonSerializer.DeserializeAsync(data, Options, cancellationToken).ConfigureAwait(false); - - return value; - } - - public T? Deserialize(ReadOnlyMemory data) where T : class => - Deserialize(data, typeof(T)) as T; -} diff --git a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs index 8a06855b9..e11097f87 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs @@ -1,14 +1,30 @@ +// ReSharper disable InconsistentNaming + #pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). +using System.Text.Json; +using System.Text.Json.Serialization; using Google.Protobuf; using Google.Protobuf.Collections; -using KurrentDB.Client.Schema.Serialization.Json; using KurrentDB.Protocol.Streams.V2; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace KurrentDB.Client; static class StreamsClientMapper { - internal static JsonSerializer JsonSerializer { get; } = new(); + static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new(JsonSerializerOptions.Default) { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + Converters = { + new MetadataJsonConverter(), + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) + } + }; public static async IAsyncEnumerable Map(this IEnumerable source) { foreach (var message in source) @@ -24,7 +40,7 @@ public static ValueTask Map(this EventData source) { metadata = new(); } else { try { - metadata = JsonSerializer.Deserialize>(source.Metadata) ?? new(); + metadata = JsonSerializer.Deserialize>(source.Metadata.Span, DefaultJsonSerializerOptions) ?? new(); } catch (Exception ex) { throw new ArgumentException( $"Event metadata must be valid JSON with property values limited to: null, boolean, number, string, Guid, DateTime, TimeSpan, or Base64-encoded byte arrays. " + diff --git a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs index 371d638ea..66009890d 100644 --- a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs +++ b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using Humanizer; using static KurrentDB.Client.Constants; -using JsonSerializer = KurrentDB.Client.Schema.Serialization.Json.JsonSerializer; namespace KurrentDB.Client.Tests.Streams; @@ -8,7 +8,6 @@ namespace KurrentDB.Client.Tests.Streams; [Trait("Category", "Operation:MultiStreamAppend")] public class MultiStreamAppendTests(ITestOutputHelper output, KurrentDBPermanentFixture fixture) : KurrentDBPermanentTests(output, fixture) { - static JsonSerializer JsonSerializer { get; } = new(); [MinimumVersion.Fact(25, 1)] public async Task append_events_with_invalid_metadata_format_throws_exceptions() { @@ -46,7 +45,7 @@ public async Task append_events_to_multiple_streams() { ByteArrayValue = "Bar"u8.ToArray() }; - var metadataBytes = JsonSerializer.Serialize(expectedMetadata); + var metadataBytes = JsonSerializer.SerializeToUtf8Bytes(expectedMetadata); AppendStreamRequest[] requests = [ new(stream1, StreamState.NoStream, Fixture.CreateTestEvents(3, metadata: metadataBytes).ToArray()), @@ -69,7 +68,7 @@ public async Task append_events_to_multiple_streams() { .ReadStreamAsync(Direction.Forwards, stream2, StreamPosition.Start, 10) .ToArrayAsync(); - var metadata = JsonSerializer.Deserialize>(stream1Events.First().OriginalEvent.Metadata); + var metadata = JsonSerializer.Deserialize>(stream1Events.First().OriginalEvent.Metadata.Span); stream1Events.Length.ShouldBe(3); stream2Events.Length.ShouldBe(2); From 449f194be7b828c5d297813e016ce3dff94f48b5 Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 19 Aug 2025 12:55:40 +0400 Subject: [PATCH 13/29] Add metadata docoder helper --- .../Core/DynamicValueMapper.cs | 4 +-- .../Streams/MetadataDecoder.cs | 29 +++++++++++++++++++ .../Streams/Model/StreamsClientMapper.cs | 17 ++--------- .../Streams/MultiStreamAppendTests.cs | 23 ++++++--------- 4 files changed, 42 insertions(+), 31 deletions(-) create mode 100644 src/KurrentDB.Client/Streams/MetadataDecoder.cs diff --git a/src/KurrentDB.Client/Core/DynamicValueMapper.cs b/src/KurrentDB.Client/Core/DynamicValueMapper.cs index cd2dd5fa9..ab3ad6b72 100644 --- a/src/KurrentDB.Client/Core/DynamicValueMapper.cs +++ b/src/KurrentDB.Client/Core/DynamicValueMapper.cs @@ -27,8 +27,8 @@ public static DynamicValue MapToDynamicValue(this object? source) { float x => new() { FloatValue = x }, double x => new() { DoubleValue = x }, - DateTime x => new() { TimestampValue = Timestamp.FromDateTime(x.Kind is DateTimeKind.Utc ? x : DateTime.SpecifyKind(x, DateTimeKind.Utc)) }, - DateTimeOffset x => new() { TimestampValue = Timestamp.FromDateTimeOffset(x) }, + DateTime x => new() { TimestampValue = x.ToTimestamp() }, + DateTimeOffset x => new() { TimestampValue = x.ToTimestamp() }, TimeSpan x => new() { DurationValue = x.ToDuration() }, byte[] x => new() { BytesValue = ByteString.CopyFrom(x) }, diff --git a/src/KurrentDB.Client/Streams/MetadataDecoder.cs b/src/KurrentDB.Client/Streams/MetadataDecoder.cs new file mode 100644 index 000000000..ae0bbc4b4 --- /dev/null +++ b/src/KurrentDB.Client/Streams/MetadataDecoder.cs @@ -0,0 +1,29 @@ +// ReSharper disable InconsistentNaming + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace KurrentDB.Client.Streams; + +public static class MetadataDecoder { + public static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerOptions.Default) { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + Converters = { + new MetadataJsonConverter(), + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) + } + }; + + public static Dictionary? Decode(ReadOnlyMemory metadataBytes) { + return JsonSerializer.Deserialize>( + metadataBytes.Span, + JsonSerializerOptions + ); + } +} diff --git a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs index e11097f87..f1c094930 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs @@ -6,26 +6,13 @@ using System.Text.Json.Serialization; using Google.Protobuf; using Google.Protobuf.Collections; +using KurrentDB.Client.Streams; using KurrentDB.Protocol.Streams.V2; using JsonSerializer = System.Text.Json.JsonSerializer; namespace KurrentDB.Client; static class StreamsClientMapper { - static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new(JsonSerializerOptions.Default) { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = false, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, - UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - Converters = { - new MetadataJsonConverter(), - new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) - } - }; - public static async IAsyncEnumerable Map(this IEnumerable source) { foreach (var message in source) yield return await message @@ -40,7 +27,7 @@ public static ValueTask Map(this EventData source) { metadata = new(); } else { try { - metadata = JsonSerializer.Deserialize>(source.Metadata.Span, DefaultJsonSerializerOptions) ?? new(); + metadata = JsonSerializer.Deserialize>(source.Metadata.Span, MetadataDecoder.JsonSerializerOptions) ?? new(); } catch (Exception ex) { throw new ArgumentException( $"Event metadata must be valid JSON with property values limited to: null, boolean, number, string, Guid, DateTime, TimeSpan, or Base64-encoded byte arrays. " + diff --git a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs index 66009890d..3c289d011 100644 --- a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs +++ b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Humanizer; +using KurrentDB.Client.Streams; using static KurrentDB.Client.Constants; namespace KurrentDB.Client.Tests.Streams; @@ -37,10 +38,8 @@ public async Task append_events_to_multiple_streams() { var expectedMetadata = new TestMetadata { StringValue = "Foo", BooleanValue = true, - Int32Value = 42, - Int64Value = 9223372036854775807L, DoubleValue = 2.718281828, - DateTimeValue = DateTime.Now, + DateTimeValue = DateTime.UtcNow, TimeSpanValue = 2.5.Hours(), ByteArrayValue = "Bar"u8.ToArray() }; @@ -68,7 +67,7 @@ public async Task append_events_to_multiple_streams() { .ReadStreamAsync(Direction.Forwards, stream2, StreamPosition.Start, 10) .ToArrayAsync(); - var metadata = JsonSerializer.Deserialize>(stream1Events.First().OriginalEvent.Metadata.Span); + var metadata = MetadataDecoder.Decode(stream1Events.First().OriginalEvent.Metadata); stream1Events.Length.ShouldBe(3); stream2Events.Length.ShouldBe(2); @@ -76,14 +75,12 @@ public async Task append_events_to_multiple_streams() { metadata.ShouldNotBeNull(); metadata[Metadata.SchemaName].ShouldBe("test-event-type"); metadata[Metadata.SchemaDataFormat].ShouldBe(SchemaDataFormat.Json); - metadata["stringValue"].ShouldBe(expectedMetadata.StringValue); - metadata["booleanValue"].ShouldBe(expectedMetadata.BooleanValue); - metadata["int32Value"].ShouldBe(expectedMetadata.Int32Value); - metadata["int64Value"].ShouldBe(expectedMetadata.Int64Value); - metadata["doubleValue"].ShouldBe(expectedMetadata.DoubleValue); - metadata["dateTimeValue"].ShouldBe(expectedMetadata.DateTimeValue); - metadata["timeSpanValue"].ShouldBe(expectedMetadata.TimeSpanValue); - metadata["byteArrayValue"].ShouldBe(expectedMetadata.ByteArrayValue); + metadata["StringValue"].ShouldBe(expectedMetadata.StringValue); + metadata["BooleanValue"].ShouldBe(expectedMetadata.BooleanValue); + metadata["DoubleValue"].ShouldBe(expectedMetadata.DoubleValue); + metadata["DateTimeValue"].ShouldBe(expectedMetadata.DateTimeValue); + metadata["TimeSpanValue"].ShouldBe(expectedMetadata.TimeSpanValue); + metadata["ByteArrayValue"].ShouldBe(expectedMetadata.ByteArrayValue); } [MinimumVersion.Fact(25, 1)] @@ -116,8 +113,6 @@ public async Task appending_events_with_failures() { public class TestMetadata { public string? StringValue { get; init; } public bool BooleanValue { get; init; } - public int Int32Value { get; init; } - public long Int64Value { get; init; } public double DoubleValue { get; init; } public DateTime DateTimeValue { get; init; } public TimeSpan TimeSpanValue { get; init; } From 235098240175dc9947b601c6fd0eddf674cdf23a Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 19 Aug 2025 12:56:21 +0400 Subject: [PATCH 14/29] Add PublicAPI attribute to MetadataDecoder class --- src/KurrentDB.Client/Streams/MetadataDecoder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/KurrentDB.Client/Streams/MetadataDecoder.cs b/src/KurrentDB.Client/Streams/MetadataDecoder.cs index ae0bbc4b4..c0296b1ac 100644 --- a/src/KurrentDB.Client/Streams/MetadataDecoder.cs +++ b/src/KurrentDB.Client/Streams/MetadataDecoder.cs @@ -2,9 +2,11 @@ using System.Text.Json; using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace KurrentDB.Client.Streams; +[PublicAPI] public static class MetadataDecoder { public static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerOptions.Default) { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, From 62717351e98d442c025c6730efb8470bb1a82900 Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 19 Aug 2025 13:48:19 +0400 Subject: [PATCH 15/29] add timespan iso 8601 converter --- .../Streams/MetadataDecoder.cs | 31 ---- .../Streams/Model/Metadata/MetadataDecoder.cs | 137 ++++++++++++++++++ .../Model/Metadata/MetadataJsonConverter.cs | 76 ---------- .../Streams/Model/StreamsClientMapper.cs | 23 +-- .../Streams/MultiStreamAppendTests.cs | 30 ++-- 5 files changed, 159 insertions(+), 138 deletions(-) delete mode 100644 src/KurrentDB.Client/Streams/MetadataDecoder.cs create mode 100644 src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs delete mode 100644 src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataJsonConverter.cs diff --git a/src/KurrentDB.Client/Streams/MetadataDecoder.cs b/src/KurrentDB.Client/Streams/MetadataDecoder.cs deleted file mode 100644 index c0296b1ac..000000000 --- a/src/KurrentDB.Client/Streams/MetadataDecoder.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ReSharper disable InconsistentNaming - -using System.Text.Json; -using System.Text.Json.Serialization; -using JetBrains.Annotations; - -namespace KurrentDB.Client.Streams; - -[PublicAPI] -public static class MetadataDecoder { - public static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerOptions.Default) { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = false, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, - UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - Converters = { - new MetadataJsonConverter(), - new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) - } - }; - - public static Dictionary? Decode(ReadOnlyMemory metadataBytes) { - return JsonSerializer.Deserialize>( - metadataBytes.Span, - JsonSerializerOptions - ); - } -} diff --git a/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs b/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs new file mode 100644 index 000000000..2db619a76 --- /dev/null +++ b/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs @@ -0,0 +1,137 @@ +// ReSharper disable InconsistentNaming + +using System.Buffers; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Xml; +using JetBrains.Annotations; +using System.Text; +using KurrentDB.Client.Core.Internal.Exceptions; +using static KurrentDB.Client.Constants; + +namespace KurrentDB.Client; + +/// +/// Provides methods to encode and decode metadata. +/// +[PublicAPI] +public static class MetadataDecoder { + public static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerOptions.Default) { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, + UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, + Converters = { + new MetadataJsonConverter(), + new TimeSpanIso8601Converter(), + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) + } + }; + + /// Decodes the provided metadata in a read-only byte memory structure into a dictionary + /// containing key-value pairs of metadata properties. + /// The read-only memory of bytes containing the metadata to decode. + /// A dictionary representing the decoded metadata, where keys are strings and values are objects, + /// or null if the decoding process fails. + /// Thrown when the metadata is not valid JSON or contains unsupported property values. + public static Dictionary Decode(ReadOnlyMemory metadata) { + try { + return JsonSerializer.Deserialize>(metadata.Span, JsonSerializerOptions) ?? throw new InvalidOperationException(); + } catch (Exception ex) { + throw new ArgumentException( + $"Event metadata must be valid JSON with property values limited to: null, boolean, number, string, Guid, DateTime, TimeSpan, or Base64-encoded byte arrays. " + + $"Complex objects and arrays are not supported. This limitation will be removed in the next major release. " + + $"Deserialization failed: {ex.Message}", + ex + ); + } + } +} + +public class MetadataJsonConverter : JsonConverter> { + public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + var metadata = new Dictionary(); + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { + if (reader.GetString() is not { } propertyName || string.IsNullOrWhiteSpace(propertyName)) + throw new JsonException("Property name cannot be empty or whitespace"); + + reader.Read(); + + var value = reader.TokenType switch { + JsonTokenType.None => null, + JsonTokenType.Null => null, + JsonTokenType.True => true, + JsonTokenType.False => false, + JsonTokenType.String => ParseString(reader, propertyName), + JsonTokenType.Number => ParseNumber(reader), + _ => throw new JsonException($"Unsupported metadata value type ({reader.TokenType}) for property '{propertyName}'") + }; + + metadata[propertyName] = value; + } + + return metadata; + + static object ParseNumber(Utf8JsonReader reader) { + if (reader.TryGetInt32(out var intValue)) + return intValue; + + if (reader.TryGetInt64(out var longValue)) + return longValue; + + if (reader.TryGetDouble(out var doubleValue)) + return doubleValue; + +#if NET48 + return Encoding.UTF8.GetString(reader.ValueSequence.ToArray()); +#else + return Encoding.UTF8.GetString(reader.ValueSpan); +#endif + } + + static object? ParseString(Utf8JsonReader reader, string propertyName) { + if (propertyName.Equals(Metadata.SchemaName, StringComparison.OrdinalIgnoreCase)) { + var value = reader.GetString(); + return string.IsNullOrWhiteSpace(value) ? "" : value; + } + + if (propertyName.Equals(Metadata.SchemaDataFormat, StringComparison.OrdinalIgnoreCase)) + return Enum.TryParse(reader.GetString(), ignoreCase: true, out var format) + ? format + : SchemaDataFormat.Unspecified; + + if (reader.TryGetGuid(out var guid)) return guid; + if (reader.TryGetDateTime(out var dateTime)) return dateTime; + if (reader.TryGetTimeSpan(out var timeSpan)) return timeSpan; + if (reader.TryGetBytesFromBase64(out var bytes)) return new ReadOnlyMemory(bytes); + + return reader.GetString(); + } + } + + public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, value, options); +} + +public class TimeSpanIso8601Converter : JsonConverter { + public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.Null) + return TimeSpan.Zero; + + if (reader.TokenType != JsonTokenType.String) + throw new JsonException("Failed to convert ISO8601 TimeSpan from JSON. Expected a string value."); + + var value = reader.GetString(); + + return string.IsNullOrEmpty(value) ? TimeSpan.Zero : XmlConvert.ToTimeSpan(value); + } + + public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) { + if (value != TimeSpan.Zero || options.DefaultIgnoreCondition is not (JsonIgnoreCondition.WhenWritingDefault or JsonIgnoreCondition.Always)) + writer.WriteStringValue(XmlConvert.ToString(value)); + } +} diff --git a/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataJsonConverter.cs b/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataJsonConverter.cs deleted file mode 100644 index ee35d9c84..000000000 --- a/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataJsonConverter.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ReSharper disable SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault - -using System.Buffers; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using KurrentDB.Client.Core.Internal.Exceptions; -using static KurrentDB.Client.Constants; - -namespace KurrentDB.Client; - -public class MetadataJsonConverter : JsonConverter> { - public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var metadata = new Dictionary(); - - while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { - if (reader.GetString() is not { } propertyName || string.IsNullOrWhiteSpace(propertyName)) - throw new JsonException("Property name cannot be empty or whitespace"); - - reader.Read(); - - var value = reader.TokenType switch { - JsonTokenType.None => null, - JsonTokenType.Null => null, - JsonTokenType.True => true, - JsonTokenType.False => false, - JsonTokenType.String => ParseString(reader, propertyName), - JsonTokenType.Number => ParseNumber(reader), - _ => throw new JsonException($"Unsupported metadata value type ({reader.TokenType}) for property '{propertyName}'") - }; - - metadata[propertyName] = value; - } - - return metadata; - - static object ParseNumber(Utf8JsonReader reader) { - if (reader.TryGetInt32(out var intValue)) - return intValue; - - if (reader.TryGetInt64(out var longValue)) - return longValue; - - if (reader.TryGetDouble(out var doubleValue)) - return doubleValue; - -#if NET48 - return Encoding.UTF8.GetString(reader.ValueSequence.ToArray()); -#else - return Encoding.UTF8.GetString(reader.ValueSpan); -#endif - } - - static object? ParseString(Utf8JsonReader reader, string propertyName) { - if (propertyName.Equals(Metadata.SchemaName, StringComparison.OrdinalIgnoreCase)) { - var value = reader.GetString(); - return string.IsNullOrWhiteSpace(value) ? "" : value; - } - - if (propertyName.Equals(Metadata.SchemaDataFormat, StringComparison.OrdinalIgnoreCase)) - return Enum.TryParse(reader.GetString(), ignoreCase: true, out var format) - ? format - : SchemaDataFormat.Unspecified; - - if (reader.TryGetGuid(out var guid)) return guid; - if (reader.TryGetDateTime(out var dateTime)) return dateTime; - if (reader.TryGetTimeSpan(out var timeSpan)) return timeSpan; - if (reader.TryGetBytesFromBase64(out var bytes)) return new ReadOnlyMemory(bytes); - - return reader.GetString(); - } - } - - public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) => - JsonSerializer.Serialize(writer, value, options); -} diff --git a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs index f1c094930..8b9839848 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs @@ -2,13 +2,9 @@ #pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). -using System.Text.Json; -using System.Text.Json.Serialization; using Google.Protobuf; using Google.Protobuf.Collections; -using KurrentDB.Client.Streams; using KurrentDB.Protocol.Streams.V2; -using JsonSerializer = System.Text.Json.JsonSerializer; namespace KurrentDB.Client; @@ -21,23 +17,10 @@ public static async IAsyncEnumerable Map(this IEnumerable Map(this EventData source) { - Dictionary metadata; + Dictionary metadata = new(); - if (source.Metadata.IsEmpty) { - metadata = new(); - } else { - try { - metadata = JsonSerializer.Deserialize>(source.Metadata.Span, MetadataDecoder.JsonSerializerOptions) ?? new(); - } catch (Exception ex) { - throw new ArgumentException( - $"Event metadata must be valid JSON with property values limited to: null, boolean, number, string, Guid, DateTime, TimeSpan, or Base64-encoded byte arrays. " + - $"Complex objects and arrays are not supported. This limitation will be removed in the next major release. " + - $"Deserialization failed: {ex.Message}", - nameof(source), - ex - ); - } - } + if (!source.Metadata.IsEmpty) + metadata = MetadataDecoder.Decode(source.Metadata); metadata[Constants.Metadata.SchemaName] = source.Type; metadata[Constants.Metadata.SchemaDataFormat] = source.ContentType is Constants.Metadata.ContentTypes.ApplicationJson diff --git a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs index 3c289d011..fe152ae4f 100644 --- a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs +++ b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs @@ -1,6 +1,5 @@ using System.Text.Json; using Humanizer; -using KurrentDB.Client.Streams; using static KurrentDB.Client.Constants; namespace KurrentDB.Client.Tests.Streams; @@ -9,6 +8,17 @@ namespace KurrentDB.Client.Tests.Streams; [Trait("Category", "Operation:MultiStreamAppend")] public class MultiStreamAppendTests(ITestOutputHelper output, KurrentDBPermanentFixture fixture) : KurrentDBPermanentTests(output, fixture) { + [Fact] + public void timespan_json_roundtrip() { + var obj = new TestMetadata { TimeSpanValue = 1.Hours().Add(30.Minutes()) }; + + string json = JsonSerializer.Serialize(obj, MetadataDecoder.JsonSerializerOptions); + json.ShouldContain("PT1H30M"); + + var deserialized = JsonSerializer.Deserialize(json, MetadataDecoder.JsonSerializerOptions); + deserialized.ShouldNotBeNull(); + deserialized.TimeSpanValue.ShouldBe(obj.TimeSpanValue); + } [MinimumVersion.Fact(25, 1)] public async Task append_events_with_invalid_metadata_format_throws_exceptions() { @@ -17,9 +27,7 @@ public async Task append_events_with_invalid_metadata_format_throws_exceptions() var invalidMetadata = "invalid"u8.ToArray(); - AppendStreamRequest[] requests = [ - new(stream, StreamState.NoStream, Fixture.CreateTestEvents(3, metadata: invalidMetadata)), - ]; + AppendStreamRequest[] requests = [new(stream, StreamState.NoStream, Fixture.CreateTestEvents(3, metadata: invalidMetadata)),]; // Act & Assert var exception = await Fixture.Streams @@ -79,7 +87,7 @@ public async Task append_events_to_multiple_streams() { metadata["BooleanValue"].ShouldBe(expectedMetadata.BooleanValue); metadata["DoubleValue"].ShouldBe(expectedMetadata.DoubleValue); metadata["DateTimeValue"].ShouldBe(expectedMetadata.DateTimeValue); - metadata["TimeSpanValue"].ShouldBe(expectedMetadata.TimeSpanValue); + // metadata["TimeSpanValue"].ShouldBe(expectedMetadata.TimeSpanValue); metadata["ByteArrayValue"].ShouldBe(expectedMetadata.ByteArrayValue); } @@ -111,10 +119,10 @@ public async Task appending_events_with_failures() { } public class TestMetadata { - public string? StringValue { get; init; } - public bool BooleanValue { get; init; } - public double DoubleValue { get; init; } - public DateTime DateTimeValue { get; init; } - public TimeSpan TimeSpanValue { get; init; } - public byte[]? ByteArrayValue { get; init; } + public string? StringValue { get; init; } + public bool? BooleanValue { get; init; } + public double? DoubleValue { get; init; } + public DateTime? DateTimeValue { get; init; } + public TimeSpan? TimeSpanValue { get; init; } + public byte[]? ByteArrayValue { get; init; } } From 994e82d2ad2802047a255e1adb416ec0a0e09b58 Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 19 Aug 2025 14:00:03 +0400 Subject: [PATCH 16/29] Update MetadataDecoder summary to reflect decode-only functionality --- .../Streams/Streams/Model/Metadata/MetadataDecoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs b/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs index 2db619a76..b74b5381d 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs @@ -12,7 +12,7 @@ namespace KurrentDB.Client; /// -/// Provides methods to encode and decode metadata. +/// Provides methods to decode metadata. /// [PublicAPI] public static class MetadataDecoder { From 59ff3689a58ef43863612dbe8c13ce69447ac99a Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 19 Aug 2025 14:37:04 +0400 Subject: [PATCH 17/29] Fix samples --- samples/appending-events/Program.cs | 10 +++++----- .../Streams/MultiStreamAppend.Extensions.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) rename {test/KurrentDB.Client.Tests => src/KurrentDB.Client}/Streams/MultiStreamAppend.Extensions.cs (97%) diff --git a/samples/appending-events/Program.cs b/samples/appending-events/Program.cs index 558ef0992..2a5db55bc 100644 --- a/samples/appending-events/Program.cs +++ b/samples/appending-events/Program.cs @@ -1,4 +1,6 @@ -#pragma warning disable CS8321 // Local function is declared but never used +using System.Text.Json; + +#pragma warning disable CS8321 // Local function is declared but never used var settings = KurrentDBClientSettings.Create("kurrentdb://localhost:2113?tls=false"); @@ -15,9 +17,7 @@ return; static async Task MultiStreamAppend(KurrentDBClient client) { - var serializer = new KurrentDB.Client.Schema.Serialization.Json.JsonSerializer(); - - var metadata = serializer.Serialize( + var metadata = JsonSerializer.SerializeToUtf8Bytes( new { TimeStamp = DateTime.UtcNow, } @@ -36,7 +36,7 @@ [new EventData(Uuid.NewUuid(), "event-one", "hello"u8.ToArray(), metadata)] ) ]; - var result = await client.MultiStreamAppendAsync(requests.ToAsyncEnumerable()); + var result = await client.MultiStreamAppendAsync(requests); if (result is MultiAppendSuccess { Successes: var successes }) foreach (var item in successes) diff --git a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppend.Extensions.cs b/src/KurrentDB.Client/Streams/MultiStreamAppend.Extensions.cs similarity index 97% rename from test/KurrentDB.Client.Tests/Streams/MultiStreamAppend.Extensions.cs rename to src/KurrentDB.Client/Streams/MultiStreamAppend.Extensions.cs index b8aa692e5..c1eff6a25 100644 --- a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppend.Extensions.cs +++ b/src/KurrentDB.Client/Streams/MultiStreamAppend.Extensions.cs @@ -1,4 +1,4 @@ -namespace KurrentDB.Client.Tests.Streams; +namespace KurrentDB.Client; public static class MultiStreamAppendExtensions { public static ValueTask MultiStreamAppendAsync( From a7049fe506e711d24cdf8da0176525182f1ab310 Mon Sep 17 00:00:00 2001 From: William Chong Date: Fri, 22 Aug 2025 15:47:54 +0400 Subject: [PATCH 18/29] Handle TimeSpan ISO8601 metadata decoder --- .../Exceptions/Utf8JsonReaderExtensions.cs | 15 +++++++++++-- .../Streams/Model/Metadata/MetadataDecoder.cs | 21 ------------------- .../Streams/MultiStreamAppendTests.cs | 14 +------------ 3 files changed, 14 insertions(+), 36 deletions(-) diff --git a/src/KurrentDB.Client/Core/Internal/Exceptions/Utf8JsonReaderExtensions.cs b/src/KurrentDB.Client/Core/Internal/Exceptions/Utf8JsonReaderExtensions.cs index 175cb03d8..c45e35b3c 100644 --- a/src/KurrentDB.Client/Core/Internal/Exceptions/Utf8JsonReaderExtensions.cs +++ b/src/KurrentDB.Client/Core/Internal/Exceptions/Utf8JsonReaderExtensions.cs @@ -1,12 +1,23 @@ using System.Globalization; using System.Text.Json; +using System.Xml; namespace KurrentDB.Client.Core.Internal.Exceptions; static class Utf8JsonReaderExtensions { public static bool TryGetTimeSpan(this ref Utf8JsonReader reader, out TimeSpan value) { - if (reader.TokenType == JsonTokenType.String && TimeSpan.TryParse(reader.GetString(), CultureInfo.InvariantCulture, out value)) - return true; + if (reader.TokenType == JsonTokenType.String) { + var str = reader.GetString(); + if (TimeSpan.TryParse(str, CultureInfo.InvariantCulture, out value)) + return true; + + try { + value = XmlConvert.ToTimeSpan(str!); + return true; + } catch (FormatException) { + // ignore + } + } value = TimeSpan.Zero; return false; diff --git a/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs b/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs index b74b5381d..84cc4638a 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Text.Json; using System.Text.Json.Serialization; -using System.Xml; using JetBrains.Annotations; using System.Text; using KurrentDB.Client.Core.Internal.Exceptions; @@ -26,7 +25,6 @@ public static class MetadataDecoder { UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, Converters = { new MetadataJsonConverter(), - new TimeSpanIso8601Converter(), new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } }; @@ -116,22 +114,3 @@ static object ParseNumber(Utf8JsonReader reader) { public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, options); } - -public class TimeSpanIso8601Converter : JsonConverter { - public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.Null) - return TimeSpan.Zero; - - if (reader.TokenType != JsonTokenType.String) - throw new JsonException("Failed to convert ISO8601 TimeSpan from JSON. Expected a string value."); - - var value = reader.GetString(); - - return string.IsNullOrEmpty(value) ? TimeSpan.Zero : XmlConvert.ToTimeSpan(value); - } - - public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) { - if (value != TimeSpan.Zero || options.DefaultIgnoreCondition is not (JsonIgnoreCondition.WhenWritingDefault or JsonIgnoreCondition.Always)) - writer.WriteStringValue(XmlConvert.ToString(value)); - } -} diff --git a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs index fe152ae4f..fef2eb84a 100644 --- a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs +++ b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs @@ -8,18 +8,6 @@ namespace KurrentDB.Client.Tests.Streams; [Trait("Category", "Operation:MultiStreamAppend")] public class MultiStreamAppendTests(ITestOutputHelper output, KurrentDBPermanentFixture fixture) : KurrentDBPermanentTests(output, fixture) { - [Fact] - public void timespan_json_roundtrip() { - var obj = new TestMetadata { TimeSpanValue = 1.Hours().Add(30.Minutes()) }; - - string json = JsonSerializer.Serialize(obj, MetadataDecoder.JsonSerializerOptions); - json.ShouldContain("PT1H30M"); - - var deserialized = JsonSerializer.Deserialize(json, MetadataDecoder.JsonSerializerOptions); - deserialized.ShouldNotBeNull(); - deserialized.TimeSpanValue.ShouldBe(obj.TimeSpanValue); - } - [MinimumVersion.Fact(25, 1)] public async Task append_events_with_invalid_metadata_format_throws_exceptions() { // Arrange @@ -87,7 +75,7 @@ public async Task append_events_to_multiple_streams() { metadata["BooleanValue"].ShouldBe(expectedMetadata.BooleanValue); metadata["DoubleValue"].ShouldBe(expectedMetadata.DoubleValue); metadata["DateTimeValue"].ShouldBe(expectedMetadata.DateTimeValue); - // metadata["TimeSpanValue"].ShouldBe(expectedMetadata.TimeSpanValue); + metadata["TimeSpanValue"].ShouldBe(expectedMetadata.TimeSpanValue); metadata["ByteArrayValue"].ShouldBe(expectedMetadata.ByteArrayValue); } From 59343b01331893434a6ac230fdd294ceb635262a Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 26 Aug 2025 09:13:42 +0400 Subject: [PATCH 19/29] [DEV-833] Support tracing in multi stream append (#364) --- .github/workflows/test.yml | 2 +- .../Diagnostics/ActivitySourceExtensions.cs | 31 +- .../Diagnostics/EventMetadataExtensions.cs | 7 + .../Diagnostics/Tracing/TracingConstants.cs | 5 +- .../Streams/KurrentDBClient.MultiAppend.cs | 62 ++-- .../Streams/Model/StreamsClientMapper.cs | 4 + .../Fixtures/DiagnosticsFixture.cs | 123 +++++++ ...ubscriptionsTracingInstrumentationTests.cs | 158 ++++++++ .../StreamsTracingInstrumentationTests.cs | 339 ++++++++++++++++++ 9 files changed, 701 insertions(+), 30 deletions(-) create mode 100644 test/KurrentDB.Client.Tests.Common/Fixtures/DiagnosticsFixture.cs create mode 100644 test/KurrentDB.Client.Tests/Diagnostics/PersistentSubscriptionsTracingInstrumentationTests.cs create mode 100644 test/KurrentDB.Client.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 472ce65de..d07368e5d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,7 @@ jobs: fail-fast: false matrix: framework: [ net8.0, net9.0 ] - test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement, Security, Misc ] + test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement, Security, Diagnostics, Misc ] runs-on: ubuntu-latest diff --git a/src/KurrentDB.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs b/src/KurrentDB.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs index 3d950a9b0..7dcc2fd66 100644 --- a/src/KurrentDB.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs +++ b/src/KurrentDB.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs @@ -1,9 +1,10 @@ +// ReSharper disable ConvertIfStatementToSwitchStatement // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract using System.Diagnostics; using KurrentDB.Diagnostics; using KurrentDB.Diagnostics.Telemetry; -using KurrentDB.Diagnostics.Tracing; +using static KurrentDB.Diagnostics.Tracing.TracingConstants; namespace KurrentDB.Client.Diagnostics; @@ -26,6 +27,30 @@ public static async ValueTask TraceClientOperation( } } + public static async ValueTask TraceMultiStreamAppend( + this ActivitySource source, + Func> tracedOperation, + ActivityTagsCollection? tags = null + ) { + using var activity = StartActivity(source, Operations.MultiAppend, ActivityKind.Client, tags, Activity.Current?.Context); + + try { + var result = await tracedOperation().ConfigureAwait(false); + + if (result is MultiAppendFailure { Failures: var failures }) { + activity?.SetStatus(ActivityStatusCode.Error); + failures.ForEach(error => activity?.AddException(error)); + return result; + } + + activity?.StatusOk(); + return result; + } catch (Exception ex) { + activity?.StatusError(ex); + throw; + } + } + public static void TraceSubscriptionEvent( this ActivitySource source, string? subscriptionId, @@ -54,7 +79,7 @@ public static void TraceSubscriptionEvent( userCredentials?.Username ?? settings.DefaultCredentials?.Username ); - StartActivity(source, TracingConstants.Operations.Subscribe, ActivityKind.Consumer, tags, parentContext) + StartActivity(source, Operations.Subscribe, ActivityKind.Consumer, tags, parentContext) ?.Dispose(); } @@ -67,7 +92,7 @@ public static void TraceSubscriptionEvent( return null; (tags ??= new ActivityTagsCollection()) - .WithRequiredTag(TelemetryTags.Database.System, "kurrent") + .WithRequiredTag(TelemetryTags.Database.System, KurrentDBClientDiagnostics.InstrumentationName) .WithRequiredTag(TelemetryTags.Database.Operation, operationName); return source diff --git a/src/KurrentDB.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs b/src/KurrentDB.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs index 89230f98f..d259c6e90 100644 --- a/src/KurrentDB.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs +++ b/src/KurrentDB.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs @@ -7,6 +7,13 @@ namespace KurrentDB.Client.Diagnostics; static class EventMetadataExtensions { + public static void InjectTracingContext(this Dictionary metadata, Activity? activity) { + if (!KurrentDBClientDiagnostics.ActivitySource.HasListeners() || activity is null) return; + + metadata[TracingConstants.Metadata.TraceId] = activity.TraceId.ToString(); + metadata[TracingConstants.Metadata.SpanId] = activity.SpanId.ToString(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlySpan InjectTracingContext( this ReadOnlyMemory eventMetadata, Activity? activity diff --git a/src/KurrentDB.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs b/src/KurrentDB.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs index 8b0de279d..5562748ad 100644 --- a/src/KurrentDB.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs +++ b/src/KurrentDB.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs @@ -4,7 +4,8 @@ namespace KurrentDB.Diagnostics.Tracing; static partial class TracingConstants { public static class Operations { - public const string Append = "streams.append"; - public const string Subscribe = "streams.subscribe"; + public const string Append = "streams.append"; + public const string MultiAppend = "streams.multi-append"; + public const string Subscribe = "streams.subscribe"; } } diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs index 17397c229..7ee9f68a9 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs @@ -1,8 +1,14 @@ // ReSharper disable SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault // ReSharper disable ConvertToPrimaryConstructor +// ReSharper disable PossibleMultipleEnumeration #pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). +using System.Diagnostics; +using KurrentDB.Client.Diagnostics; +using KurrentDB.Diagnostics; +using KurrentDB.Diagnostics.Telemetry; +using KurrentDB.Diagnostics.Tracing; using static KurrentDB.Protocol.Streams.V2.StreamsService; using static KurrentDB.Protocol.Streams.V2.MultiStreamAppendResponse; using Contracts = KurrentDB.Protocol.Streams.V2; @@ -34,35 +40,43 @@ public async ValueTask MultiStreamAppendAsync( var client = new StreamsServiceClient(channelInfo.CallInvoker); - using var session = client.MultiStreamAppendSession(KurrentDBCallOptions.CreateStreaming(Settings, cancellationToken: cancellationToken)); + var tags = new ActivityTagsCollection() + .WithGrpcChannelServerTags(channelInfo) + .WithClientSettingsServerTags(Settings) + .WithOptionalTag(TelemetryTags.Database.User, Settings.DefaultCredentials?.Username); - await foreach (var request in requests.WithCancellation(cancellationToken)) { - var records = await request.Messages - .Map() - .ToArrayAsync(cancellationToken) - .ConfigureAwait(false); + return await KurrentDBClientDiagnostics.ActivitySource.TraceMultiStreamAppend(Operation, tags); - var serviceRequest = new Contracts.AppendStreamRequest { - Stream = request.Stream, - ExpectedRevision = request.ExpectedState.ToInt64(), - Records = { records } - }; + async ValueTask Operation() { + using var session = client.MultiStreamAppendSession(KurrentDBCallOptions.CreateStreaming(Settings, cancellationToken: cancellationToken)); - // Cancellation of stream writes is not supported by this gRPC implementation. - // To cancel the operation, we should cancel the entire session. - await session.RequestStream - .WriteAsync(serviceRequest) - .ConfigureAwait(false); - } + await foreach (var request in requests.WithCancellation(cancellationToken)) { + var records = await request.Messages + .Map() + .ToArrayAsync(cancellationToken) + .ConfigureAwait(false); + + var serviceRequest = new Contracts.AppendStreamRequest { + Stream = request.Stream, + ExpectedRevision = request.ExpectedState.ToInt64(), + Records = { records } + }; + + // Cancellation of stream writes is not supported by this gRPC implementation. + // To cancel the operation, we should cancel the entire session. + await session.RequestStream + .WriteAsync(serviceRequest) + .ConfigureAwait(false); + } - await session.RequestStream.CompleteAsync(); + await session.RequestStream.CompleteAsync(); - var response = await session.ResponseAsync; + var response = await session.ResponseAsync; - return response.ResultCase switch { - ResultOneofCase.Success => new MultiAppendSuccess(response.Success.Map()), - ResultOneofCase.Failure => new MultiAppendFailure(response.Failure.Map()) - }; + return response.ResultCase switch { + ResultOneofCase.Success => new MultiAppendSuccess(response.Success.Map()), + ResultOneofCase.Failure => new MultiAppendFailure(response.Failure.Map()) + }; + } } } - diff --git a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs index 8b9839848..407299b0d 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs @@ -2,8 +2,10 @@ #pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). +using System.Diagnostics; using Google.Protobuf; using Google.Protobuf.Collections; +using KurrentDB.Client.Diagnostics; using KurrentDB.Protocol.Streams.V2; namespace KurrentDB.Client; @@ -27,6 +29,8 @@ public static ValueTask Map(this EventData source) { ? SchemaDataFormat.Json : SchemaDataFormat.Bytes; + metadata.InjectTracingContext(Activity.Current); + var record = new AppendRecord { RecordId = source.EventId.ToString(), Data = ByteString.CopyFrom(source.Data.Span), diff --git a/test/KurrentDB.Client.Tests.Common/Fixtures/DiagnosticsFixture.cs b/test/KurrentDB.Client.Tests.Common/Fixtures/DiagnosticsFixture.cs new file mode 100644 index 000000000..1ef5c9a7a --- /dev/null +++ b/test/KurrentDB.Client.Tests.Common/Fixtures/DiagnosticsFixture.cs @@ -0,0 +1,123 @@ +// ReSharper disable InconsistentNaming + +using System.Collections.Concurrent; +using System.Diagnostics; +using KurrentDB.Client.Diagnostics; +using KurrentDB.Client.Tests.TestNode; +using KurrentDB.Diagnostics; +using KurrentDB.Diagnostics.Telemetry; +using KurrentDB.Diagnostics.Tracing; + +namespace KurrentDB.Client.Tests.Fixtures; + +public class DiagnosticsFixture : KurrentDBPermanentFixture { + readonly ConcurrentDictionary<(string Operation, ActivityTraceId TraceId), List> Activities = []; + + public DiagnosticsFixture() : base(x => x.RunProjections()) { + var diagnosticActivityListener = new ActivityListener { + ShouldListenTo = source => source.Name == KurrentDBClientDiagnostics.InstrumentationName, + Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = activity => { + var operation = (string?)activity.GetTagItem(TelemetryTags.Database.Operation); + + if (operation is null) + return; + + Activities.AddOrUpdate( + (operation, activity.TraceId), + _ => [activity], + (_, activities) => { + activities.Add(activity); + return activities; + } + ); + } + }; + + OnSetup += () => { + ActivitySource.AddActivityListener(diagnosticActivityListener); + return Task.CompletedTask; + }; + + OnTearDown = () => { + diagnosticActivityListener.Dispose(); + return Task.CompletedTask; + }; + } + + public ActivityTraceId CreateTraceId() { + var activity = new Activity(Guid.NewGuid().ToString("N")); + activity.Start(); + Activity.Current = activity; + return activity.TraceId; + } + + public List GetActivities(string operation, ActivityTraceId traceId) => + Activities.TryGetValue((operation, traceId), out var activities) ? activities : []; + + public void AssertMultiAppendActivityHasExpectedTags(Activity activity) { + var expectedTags = new Dictionary { + { TelemetryTags.Database.System, KurrentDBClientDiagnostics.InstrumentationName }, + { TelemetryTags.Database.Operation, TracingConstants.Operations.MultiAppend }, + { TelemetryTags.Database.User, TestCredentials.Root.Username }, + { TelemetryTags.Otel.StatusCode, ActivityStatusCodeHelper.OkStatusCodeTagValue } + }; + + foreach (var tag in expectedTags) + activity.Tags.ShouldContain(tag); + } + + public void AssertAppendActivityHasExpectedTags(Activity activity, string stream) { + var expectedTags = new Dictionary { + { TelemetryTags.Database.System, KurrentDBClientDiagnostics.InstrumentationName }, + { TelemetryTags.Database.Operation, TracingConstants.Operations.Append }, + { TelemetryTags.KurrentDB.Stream, stream }, + { TelemetryTags.Database.User, TestCredentials.Root.Username }, + { TelemetryTags.Otel.StatusCode, ActivityStatusCodeHelper.OkStatusCodeTagValue } + }; + + foreach (var tag in expectedTags) + activity.Tags.ShouldContain(tag); + } + + public void AssertErroneousAppendActivityHasExpectedTags(Activity activity, Exception actualException) { + var expectedTags = new Dictionary { + { TelemetryTags.Otel.StatusCode, ActivityStatusCodeHelper.ErrorStatusCodeTagValue } + }; + + foreach (var tag in expectedTags) + activity.Tags.ShouldContain(tag); + + var actualEvent = activity.Events.ShouldHaveSingleItem(); + + actualEvent.Name.ShouldBe(TelemetryTags.Exception.EventName); + actualEvent.Tags.ShouldContain(new KeyValuePair(TelemetryTags.Exception.Type, actualException.GetType().FullName)); + + actualEvent.Tags.ShouldContain(new KeyValuePair(TelemetryTags.Exception.Message, actualException.Message)); + + actualEvent.Tags.Any(x => x.Key == TelemetryTags.Exception.Stacktrace).ShouldBeTrue(); + } + + public void AssertSubscriptionActivityHasExpectedTags( + Activity activity, + string stream, + string eventId, + string? subscriptionId = null + ) { + var expectedTags = new Dictionary { + { TelemetryTags.Database.System, KurrentDBClientDiagnostics.InstrumentationName }, + { TelemetryTags.Database.Operation, TracingConstants.Operations.Subscribe }, + { TelemetryTags.KurrentDB.Stream, stream }, + { TelemetryTags.KurrentDB.EventId, eventId }, + { TelemetryTags.KurrentDB.EventType, TestEventType }, + { TelemetryTags.Database.User, TestCredentials.Root.Username } + }; + + if (subscriptionId != null) + expectedTags[TelemetryTags.KurrentDB.SubscriptionId] = subscriptionId; + + foreach (var tag in expectedTags) { + activity.Tags.ShouldContain(tag); + } + } +} diff --git a/test/KurrentDB.Client.Tests/Diagnostics/PersistentSubscriptionsTracingInstrumentationTests.cs b/test/KurrentDB.Client.Tests/Diagnostics/PersistentSubscriptionsTracingInstrumentationTests.cs new file mode 100644 index 000000000..a2e95bb11 --- /dev/null +++ b/test/KurrentDB.Client.Tests/Diagnostics/PersistentSubscriptionsTracingInstrumentationTests.cs @@ -0,0 +1,158 @@ +using KurrentDB.Client.Tests.Fixtures; +using KurrentDB.Client.Tests.TestNode; +using KurrentDB.Diagnostics.Tracing; + +namespace KurrentDB.Client.Tests.Diagnostics; + +[Trait("Category", "Target:Diagnostics")] +public class PersistentSubscriptionsTracingInstrumentationTests(ITestOutputHelper output, DiagnosticsFixture fixture) + : KurrentDBPermanentTests(output, fixture) { + [RetryFact] + public async Task persistent_subscription_restores_remote_append_context() { + var traceId = Fixture.CreateTraceId(); + var stream = Fixture.GetStreamName(); + var events = Fixture.CreateTestEvents(2, metadata: Fixture.CreateTestJsonMetadata()).ToArray(); + + var groupName = $"{stream}-group"; + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + groupName, + new() + ); + + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.NoStream, + events + ); + + string? subscriptionId = null; + await Subscribe().WithTimeout(); + + var appendActivity = Fixture + .GetActivities(TracingConstants.Operations.Append, traceId) + .SingleOrDefault() + .ShouldNotBeNull(); + + var subscribeActivities = Fixture + .GetActivities(TracingConstants.Operations.Subscribe, traceId) + .ToArray(); + + subscriptionId.ShouldNotBeNull(); + subscribeActivities.Length.ShouldBe(events.Length); + + for (var i = 0; i < subscribeActivities.Length; i++) { + subscribeActivities[i].TraceId.ShouldBe(appendActivity.Context.TraceId); + subscribeActivities[i].ParentSpanId.ShouldBe(appendActivity.Context.SpanId); + subscribeActivities[i].HasRemoteParent.ShouldBeTrue(); + + Fixture.AssertSubscriptionActivityHasExpectedTags( + subscribeActivities[i], + stream, + events[i].EventId.ToString(), + subscriptionId + ); + } + + return; + + async Task Subscribe() { + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, groupName); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + int eventsAppeared = 0; + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is PersistentSubscriptionMessage.SubscriptionConfirmation(var sid)) + subscriptionId = sid; + + if (enumerator.Current is not PersistentSubscriptionMessage.Event(_, _)) + continue; + + eventsAppeared++; + if (eventsAppeared >= events.Length) + return; + } + } + } + + [RetryFact] + public async Task persistent_subscription_handles_non_json_events() { + var stream = Fixture.GetStreamName(); + var events = Fixture.CreateTestEvents( + 2, + metadata: Fixture.CreateTestJsonMetadata(), + contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream + ).ToArray(); + + var groupName = $"{stream}-group"; + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + groupName, + new() + ); + + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.NoStream, + events + ); + + await Subscribe().WithTimeout(); + + return; + + async Task Subscribe() { + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, groupName); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + var eventsAppeared = 0; + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is PersistentSubscriptionMessage.Event(_, _)) + eventsAppeared++; + + if (eventsAppeared >= events.Length) + return; + } + } + } + + [RetryFact] + public async Task persistent_subscription_handles_invalid_json_metadata() { + var stream = Fixture.GetStreamName(); + var events = Fixture.CreateTestEvents( + 2, + metadata: "clearlynotavalidjsonobject"u8.ToArray() + ).ToArray(); + + var groupName = $"{stream}-group"; + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + groupName, + new() + ); + + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.NoStream, + events + ); + + await Subscribe().WithTimeout(); + + return; + + async Task Subscribe() { + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, groupName); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + var eventsAppeared = 0; + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is PersistentSubscriptionMessage.Event(_, _)) + eventsAppeared++; + + if (eventsAppeared >= events.Length) + return; + } + } + } +} diff --git a/test/KurrentDB.Client.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs b/test/KurrentDB.Client.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs new file mode 100644 index 000000000..4e3b23f15 --- /dev/null +++ b/test/KurrentDB.Client.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs @@ -0,0 +1,339 @@ +// ReSharper disable AccessToDisposedClosure + +using System.Diagnostics; +using KurrentDB.Client.Diagnostics; +using KurrentDB.Client.Tests.Fixtures; +using KurrentDB.Diagnostics.Telemetry; +using KurrentDB.Diagnostics.Tracing; + +namespace KurrentDB.Client.Tests.Diagnostics; + +[Trait("Category", "Target:Diagnostics")] +public class StreamsTracingInstrumentationTests(ITestOutputHelper output, DiagnosticsFixture fixture) + : KurrentDBPermanentTests(output, fixture) { + [Fact] + public async Task append_to_stream() { + var traceId = Fixture.CreateTraceId(); + + var stream = Fixture.GetStreamName(); + + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.NoStream, + Fixture.CreateTestEvents() + ); + + var activity = Fixture + .GetActivities(TracingConstants.Operations.Append, traceId) + .SingleOrDefault() + .ShouldNotBeNull(); + + Fixture.AssertAppendActivityHasExpectedTags(activity, stream); + } + + [MinimumVersion.Fact(25, 1)] + public async Task multi_stream_append() { + // Arrange + var traceId = Fixture.CreateTraceId(); + + var seedEvents = Fixture.CreateTestEvents(10).ToList(); + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + var stream1 = Fixture.GetStreamName(); + var stream2 = Fixture.GetStreamName(); + + AppendStreamRequest[] requests = [new(stream1, StreamState.NoStream, seedEvents.Take(5)), new(stream2, StreamState.NoStream, seedEvents.Skip(5))]; + + // Act + var multiStreamAppendResult = await Fixture.Streams.MultiStreamAppendAsync(requests.ToAsyncEnumerable()); + + await using var subscription = Fixture.Streams.SubscribeToAll( + FromAll.Start, + filterOptions: new SubscriptionFilterOptions(StreamFilter.Prefix(stream1, stream2)) + ); + + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await Subscribe().WithTimeout(); + + // Assert + multiStreamAppendResult.IsSuccess.ShouldBeTrue(); + + var appendActivities = Fixture.GetActivities(TracingConstants.Operations.MultiAppend, traceId); + var subscribeActivities = Fixture.GetActivities(TracingConstants.Operations.Subscribe, traceId); + + appendActivities.ShouldNotBeEmpty(); + subscribeActivities.ShouldNotBeEmpty(); + + appendActivities.Count.ShouldBe(1); + subscribeActivities.Count.ShouldBe(10); + + // They also have the same duration + appendActivities.Select(x => x.Duration).Distinct().Count().ShouldBe(1); + + // Check that subscribe activities have the correct parent IDs inherited from append activities + subscribeActivities + .FirstOrDefault(x => x.ParentId == appendActivities.First().Id)?.ParentSpanId + .ShouldBe(appendActivities.First().SpanId); + + subscribeActivities + .FirstOrDefault(x => x.ParentId == appendActivities.Last().Id)?.ParentSpanId + .ShouldBe(appendActivities.Last().SpanId); + + subscribeActivities + .All(x => x.StartTimeUtc > appendActivities.First().StartTimeUtc) + .ShouldBeTrue(); + + Fixture.AssertMultiAppendActivityHasExpectedTags(appendActivities.First()); + Fixture.AssertSubscriptionActivityHasExpectedTags(subscribeActivities.First(), stream1, seedEvents.First().EventId.ToString()); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) + continue; + + availableEvents.Remove(resolvedEvent.Event.EventId); + + if (availableEvents.Count is 0) + return; + } + } + } + + [MinimumVersion.Fact(25, 1)] + public async Task multi_stream_append_with_failures() { + var traceId = Fixture.CreateTraceId(); + + // Arrange + var stream1 = Fixture.GetStreamName(); + var stream2 = Fixture.GetStreamName(); + + AppendStreamRequest[] requests = [ + new(stream1, StreamState.StreamExists, Fixture.CreateTestEvents()), + new(stream2, StreamState.StreamExists, Fixture.CreateTestEvents()) + ]; + + // Act + var multiStreamAppendResult = await Fixture.Streams.MultiStreamAppendAsync(requests); + + // Assert + multiStreamAppendResult.IsFailure.ShouldBeTrue(); + + var appendActivities = Fixture.GetActivities(TracingConstants.Operations.MultiAppend, traceId); + + appendActivities.ShouldNotBeEmpty(); + + appendActivities.Count.ShouldBe(1); + + var activity = appendActivities.FirstOrDefault().ShouldNotBeNull(); + activity.Status.ShouldBe(ActivityStatusCode.Error); + activity.Events.Count().ShouldBe(2); + + activity.Events.ShouldAllBe(activityEvent => + activityEvent.Tags.Any(tag => tag.Key == TelemetryTags.Exception.Message) && + activityEvent.Tags.Any(tag => tag.Key == TelemetryTags.Exception.Stacktrace) && + activityEvent.Tags.Any(tag => tag.Key == TelemetryTags.Exception.Type && (string?)tag.Value == typeof(WrongExpectedVersionException).FullName) + ); + } + + [Fact] + public async Task append_trace_tagged_with_error_on_exception() { + var traceId = Fixture.CreateTraceId(); + var stream = Fixture.GetStreamName(); + + var actualException = await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.NoStream, + Fixture.CreateTestEventsThatThrowsException() + ).ShouldThrowAsync(); + + var activity = Fixture + .GetActivities(TracingConstants.Operations.Append, traceId) + .SingleOrDefault() + .ShouldNotBeNull(); + + Fixture.AssertErroneousAppendActivityHasExpectedTags(activity, actualException); + } + + [Fact] + public async Task tracing_context_injected_when_metadata_is_json() { + var traceId = Fixture.CreateTraceId(); + var stream = Fixture.GetStreamName(); + + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.NoStream, + Fixture.CreateTestEvents(1, metadata: Fixture.CreateTestJsonMetadata()) + ); + + var activity = Fixture + .GetActivities(TracingConstants.Operations.Append, traceId) + .SingleOrDefault() + .ShouldNotBeNull(); + + var readResult = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start) + .ToListAsync(); + + var tracingMetadata = readResult[0].OriginalEvent.Metadata.ExtractTracingMetadata(); + + tracingMetadata.ShouldNotBe(TracingMetadata.None); + tracingMetadata.TraceId.ShouldBe(activity.TraceId.ToString()); + tracingMetadata.SpanId.ShouldBe(activity.SpanId.ToString()); + } + + [Fact] + public async Task tracing_context_not_injected_when_metadata_not_json() { + var stream = Fixture.GetStreamName(); + + var inputMetadata = "clearlynotavalidjsonobject"u8.ToArray(); + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.NoStream, + Fixture.CreateTestEvents(1, metadata: inputMetadata) + ); + + var readResult = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start) + .ToListAsync(); + + var outputMetadata = readResult[0].OriginalEvent.Metadata.ToArray(); + outputMetadata.ShouldBe(inputMetadata); + } + + [Fact] + public async Task tracing_context_injected_when_event_not_json_but_metadata_json() { + var traceId = Fixture.CreateTraceId(); + var stream = Fixture.GetStreamName(); + + var inputMetadata = Fixture.CreateTestJsonMetadata().ToArray(); + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.NoStream, + Fixture.CreateTestEvents( + metadata: inputMetadata, + contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream + ) + ); + + var readResult = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start) + .ToListAsync(); + + var outputMetadata = readResult[0].OriginalEvent.Metadata.ToArray(); + outputMetadata.ShouldNotBe(inputMetadata); + + var appendActivities = Fixture.GetActivities(TracingConstants.Operations.Append, traceId); + + appendActivities.ShouldNotBeEmpty(); + } + + [Fact] + public async Task json_metadata_traced_non_json_metadata_not_traced() { + var traceId = Fixture.CreateTraceId(); + var streamName = Fixture.GetStreamName(); + + var seedEvents = new[] { + Fixture.CreateTestEvent(metadata: Fixture.CreateTestJsonMetadata()), + Fixture.CreateTestEvent(metadata: Fixture.CreateTestNonJsonMetadata()) + }; + + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); + + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + + await using var subscription = Fixture.Streams.SubscribeToStream(streamName, FromStream.Start); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + var appendActivities = Fixture + .GetActivities(TracingConstants.Operations.Append, traceId) + .ShouldNotBeNull(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); + + await Subscribe(enumerator).WithTimeout(); + + var subscribeActivities = Fixture + .GetActivities(TracingConstants.Operations.Subscribe, traceId) + .ToArray(); + + appendActivities.ShouldHaveSingleItem(); + + subscribeActivities.ShouldHaveSingleItem(); + + subscribeActivities.First().ParentId.ShouldBe(appendActivities.First().Id); + + var jsonMetadataEvent = seedEvents.First(); + + Fixture.AssertSubscriptionActivityHasExpectedTags( + subscribeActivities.First(), + streamName, + jsonMetadataEvent.EventId.ToString() + ); + + return; + + async Task Subscribe(IAsyncEnumerator internalEnumerator) { + while (await internalEnumerator.MoveNextAsync()) { + if (internalEnumerator.Current is not StreamMessage.Event(var resolvedEvent)) + continue; + + availableEvents.Remove(resolvedEvent.Event.EventId); + + if (availableEvents.Count == 0) + return; + } + } + } + + [RetryFact] + [Trait("Category", "Special cases")] + public async Task no_trace_when_event_is_null() { + var traceId = Fixture.CreateTraceId(); + var category = Guid.NewGuid().ToString("N"); + var streamName = category + "-123"; + + var seedEvents = Fixture.CreateTestEvents(type: $"{category}-{Fixture.GetStreamName()}").ToArray(); + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); + + await Fixture.Streams.DeleteAsync(streamName, StreamState.StreamExists); + + await using var subscription = Fixture.Streams.SubscribeToStream("$ce-" + category, FromStream.Start, resolveLinkTos: true); + + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + Assert.True(await enumerator.MoveNextAsync()); + + Assert.IsType(enumerator.Current); + + await Subscribe().WithTimeout(); + + var appendActivities = Fixture + .GetActivities(TracingConstants.Operations.Append, traceId) + .ShouldNotBeNull(); + + var subscribeActivities = Fixture + .GetActivities(TracingConstants.Operations.Subscribe, traceId) + .ToArray(); + + appendActivities.ShouldHaveSingleItem(); + subscribeActivities.ShouldBeEmpty(); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) + continue; + + if (resolvedEvent.Event?.EventType is "$metadata") + return; + } + } + } +} From abb5bcf177f506902ee72af3617523de88f5bced Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 26 Aug 2025 11:47:19 +0400 Subject: [PATCH 20/29] Expand multi stream append tests to cover additional metadata types and null/zero TimeSpan values --- .../Streams/KurrentDBClient.MultiAppend.cs | 1 - .../Streams/MultiStreamAppendTests.cs | 43 +++++++++++++------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs index 7ee9f68a9..1ebb2a4b6 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs @@ -8,7 +8,6 @@ using KurrentDB.Client.Diagnostics; using KurrentDB.Diagnostics; using KurrentDB.Diagnostics.Telemetry; -using KurrentDB.Diagnostics.Tracing; using static KurrentDB.Protocol.Streams.V2.StreamsService; using static KurrentDB.Protocol.Streams.V2.MultiStreamAppendResponse; using Contracts = KurrentDB.Protocol.Streams.V2; diff --git a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs index fef2eb84a..0937b09b3 100644 --- a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs +++ b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs @@ -32,12 +32,15 @@ public async Task append_events_to_multiple_streams() { var stream2 = Fixture.GetStreamName(); var expectedMetadata = new TestMetadata { - StringValue = "Foo", - BooleanValue = true, - DoubleValue = 2.718281828, - DateTimeValue = DateTime.UtcNow, - TimeSpanValue = 2.5.Hours(), - ByteArrayValue = "Bar"u8.ToArray() + StringValue = "Foo", + IntegerValue = 0, + BooleanValue = true, + DoubleValue = 2.718281828, + DateTimeValue = DateTime.UtcNow, + TimeSpanValue = 2.5.Hours(), + NullTimeSpanValue = null, + ZeroTimeSpanValue = TimeSpan.Zero, + ByteArrayValue = "Bar"u8.ToArray() }; var metadataBytes = JsonSerializer.SerializeToUtf8Bytes(expectedMetadata); @@ -73,10 +76,23 @@ public async Task append_events_to_multiple_streams() { metadata[Metadata.SchemaDataFormat].ShouldBe(SchemaDataFormat.Json); metadata["StringValue"].ShouldBe(expectedMetadata.StringValue); metadata["BooleanValue"].ShouldBe(expectedMetadata.BooleanValue); + metadata["IntegerValue"].ShouldBe(expectedMetadata.IntegerValue); metadata["DoubleValue"].ShouldBe(expectedMetadata.DoubleValue); metadata["DateTimeValue"].ShouldBe(expectedMetadata.DateTimeValue); metadata["TimeSpanValue"].ShouldBe(expectedMetadata.TimeSpanValue); + metadata["NullTimeSpanValue"].ShouldBeNull(); + metadata["ZeroTimeSpanValue"].ShouldBe(expectedMetadata.ZeroTimeSpanValue); metadata["ByteArrayValue"].ShouldBe(expectedMetadata.ByteArrayValue); + + metadata["BooleanValue"]?.GetType().ShouldBe(typeof(bool)); + metadata["StringValue"]?.GetType().ShouldBe(typeof(string)); + metadata["IntegerValue"]?.GetType().ShouldBe(typeof(int)); + metadata["DoubleValue"]?.GetType().ShouldBe(typeof(double)); + metadata["DateTimeValue"]?.GetType().ShouldBe(typeof(DateTime)); + metadata["TimeSpanValue"]?.GetType().ShouldBe(typeof(TimeSpan)); + metadata["NullTimeSpanValue"]?.GetType().ShouldBe(typeof(TimeSpan)); + metadata["ZeroTimeSpanValue"]?.GetType().ShouldBe(typeof(TimeSpan)); + metadata["ByteArrayValue"]?.GetType().ShouldBe(typeof(ReadOnlyMemory)); } [MinimumVersion.Fact(25, 1)] @@ -107,10 +123,13 @@ public async Task appending_events_with_failures() { } public class TestMetadata { - public string? StringValue { get; init; } - public bool? BooleanValue { get; init; } - public double? DoubleValue { get; init; } - public DateTime? DateTimeValue { get; init; } - public TimeSpan? TimeSpanValue { get; init; } - public byte[]? ByteArrayValue { get; init; } + public string? StringValue { get; init; } + public int? IntegerValue { get; init; } + public bool? BooleanValue { get; init; } + public double? DoubleValue { get; init; } + public DateTime? DateTimeValue { get; init; } + public TimeSpan? TimeSpanValue { get; init; } + public TimeSpan? NullTimeSpanValue { get; init; } + public TimeSpan? ZeroTimeSpanValue { get; init; } + public byte[]? ByteArrayValue { get; init; } } From 62330c9a10f48a232531663f1b013a8845484267 Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 26 Aug 2025 14:39:16 +0400 Subject: [PATCH 21/29] Update documentation --- docs/api/appending-events.md | 60 ++++++++++++++---------------------- 1 file changed, 23 insertions(+), 37 deletions(-) diff --git a/docs/api/appending-events.md b/docs/api/appending-events.md index 41d030ffb..00cc27654 100644 --- a/docs/api/appending-events.md +++ b/docs/api/appending-events.md @@ -157,9 +157,23 @@ The operation returns either: - `MultiAppendFailure` - Specific exceptions for any failed operations ::: warning -Event metadata in `EventData` must be valid JSON deserializable to -`Dictionary`. This requirement will be removed in a future -major release. +Event metadata in `EventData` must currently be valid JSON that can be deserialized into a +`Dictionary`. This means any metadata you attach to an event should be structured as a JSON object, not as a primitive value or array. + +For example, to encode metadata: +```cs +var metadata = JsonSerializer.SerializeToUtf8Bytes(new { + Timestamp = DateTime.UtcNow, + Source = "OrderProcessingSystem", + Version = 1.0 +}); +``` +To decode metadata back into a dictionary: +```cs +var dictionary = MetadataDecoder.Decode(metadataBytes); +``` + +This requirement ensures compatibility with KurrentDB's current metadata handling. In a future major release, this restriction will be lifted, allowing more flexible metadata formats. ::: Here's a basic example of appending events to multiple streams: @@ -167,47 +181,19 @@ Here's a basic example of appending events to multiple streams: ```cs using System.Text.Json; -var metadata = JsonSerializer.SerializeToUtf8Bytes( - new { - Timestamp = DateTime.UtcNow, - Source = "OrderProcessingSystem", - Version = 1.0 - } -); - AppendStreamRequest[] requests = [ new( - "order-stream-1", + "order-stream", StreamState.Any, [ - new EventData( - Uuid.NewUuid(), - "OrderCreated", - JsonSerializer.SerializeToUtf8Bytes( - new { - OrderId = "12345", - Amount = 99.99 - } - ), - metadata - ) + new EventData(Uuid.NewUuid(), "OrderCreated", Encoding.UTF8.GetBytes("{\"orderId\": \"21345\", \"amount\": 99.99}")) ] ), new( - "inventory-stream-1", + "inventory-stream", StreamState.Any, [ - new EventData( - Uuid.NewUuid(), - "ItemReserved", - JsonSerializer.SerializeToUtf8Bytes( - new { - ItemId = "ABC123", - Quantity = 2 - } - ), - metadata - ) + new EventData(Uuid.NewUuid(), "ItemReserved", Encoding.UTF8.GetBytes("{\"itemId\": \"abc123\", \"quantity\": 2}")) ] ) ]; @@ -219,10 +205,10 @@ if (result is MultiAppendSuccess { Successes: var successes }) Console.WriteLine($"Stream '{item.Stream}' updated at position {item.Position}"); ``` -If the operation doesn't succeed, it can fail with the following exceptions: +If the operation doesn't succeed, you should check if the result is a `MultiAppendFailure` and handle each failure case appropriately. This allows you to respond to specific errors, such as version conflicts, access issues, deleted streams, or transaction size limits. For example: ```cs -var result = await client.MultiStreamAppendAsync(requests.ToAsyncEnumerable()); +var result = await client.MultiStreamAppendAsync(requests); if (result is MultiAppendFailure { Failures: var failures }) { foreach (var error in failures) { From a6ac9203545ad4f4175f536573b9fce248281ba3 Mon Sep 17 00:00:00 2001 From: William Chong Date: Mon, 29 Sep 2025 16:19:00 +0400 Subject: [PATCH 22/29] Adapt client to revised db feature changes --- samples/appending-events/Program.cs | 22 +- src/KurrentDB.Client/Core/Common/Constants.cs | 10 +- .../Diagnostics/ActivitySourceExtensions.cs | 24 - .../Core/Common/ValueExtensions.cs | 274 ++++++++++ .../Core/DynamicValueMapper.cs | 40 -- .../WrongExpectedVersionException.cs | 8 +- .../Core/GrpcServerCapabilitiesClient.cs | 2 +- src/KurrentDB.Client/Core/ValueMapper.cs | 44 ++ .../proto/kurrentdb/protocol/v1/code.proto | 186 ------- .../proto/kurrentdb/protocol/v1/status.proto | 48 -- .../proto/kurrentdb/protocol/v1/streams.proto | 2 +- .../proto/kurrentdb/protocol/v2/core.proto | 129 ----- .../proto/kurrentdb/protocol/v2/errors.proto | 147 +++++ .../proto/kurrentdb/protocol/v2/rpc.proto | 91 +++ .../protocol/v2/streams/error_details.proto | 88 --- .../protocol/v2/streams/errors.proto | 127 +++++ .../protocol/v2/streams/streams.proto | 517 +++--------------- src/KurrentDB.Client/KurrentDB.Client.csproj | 9 +- ...entDBPersistentSubscriptionsClient.Read.cs | 2 +- .../Streams/KurrentDBClient.Append.cs | 2 +- .../Streams/KurrentDBClient.MultiAppend.cs | 69 +-- .../Streams/KurrentDBClient.Read.cs | 4 +- .../Streams/KurrentDBClient.Subscriptions.cs | 2 +- .../Streams/MultiStreamAppend.Extensions.cs | 10 +- .../Streams/Streams/BatchAppendResp.cs | 20 +- .../Streams/Model/Metadata/MetadataDecoder.cs | 37 +- .../Streams/Model/StreamsClientMapper.cs | 89 ++- .../Streams/Model/StreamsClientModel.cs | 52 +- .../StreamsTracingInstrumentationTests.cs | 27 +- .../Streams/MultiStreamAppendTests.cs | 66 ++- 30 files changed, 971 insertions(+), 1177 deletions(-) create mode 100644 src/KurrentDB.Client/Core/Common/ValueExtensions.cs delete mode 100644 src/KurrentDB.Client/Core/DynamicValueMapper.cs create mode 100644 src/KurrentDB.Client/Core/ValueMapper.cs delete mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/code.proto delete mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/status.proto delete mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/core.proto create mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/errors.proto create mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/rpc.proto delete mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/error_details.proto create mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/errors.proto diff --git a/samples/appending-events/Program.cs b/samples/appending-events/Program.cs index 2a5db55bc..c248d1a8d 100644 --- a/samples/appending-events/Program.cs +++ b/samples/appending-events/Program.cs @@ -36,27 +36,7 @@ [new EventData(Uuid.NewUuid(), "event-one", "hello"u8.ToArray(), metadata)] ) ]; - var result = await client.MultiStreamAppendAsync(requests); - - if (result is MultiAppendSuccess { Successes: var successes }) - foreach (var item in successes) - Console.WriteLine($"Stream: {item.Stream}, Position: {item.Position}"); - - else if (result is MultiAppendFailure { Failures: var failures }) - foreach (var item in failures) - switch (item) { - case WrongExpectedVersionException ex: - Console.WriteLine($"Stream: {ex.StreamName}, Expected revision: {ex.ActualStreamState}"); - break; - - case TransactionMaxSizeExceededException ex: - Console.WriteLine($"Transaction size exceeded the maximum allowed size of {ex.MaxSize} bytes."); - break; - - case StreamDeletedException ex: - Console.WriteLine($"Stream: {ex.Stream} has been deleted."); - break; - } + await client.MultiStreamAppendAsync(requests); } static async Task AppendToStream(KurrentDBClient client) { diff --git a/src/KurrentDB.Client/Core/Common/Constants.cs b/src/KurrentDB.Client/Core/Common/Constants.cs index 925b01619..8637c9af4 100644 --- a/src/KurrentDB.Client/Core/Common/Constants.cs +++ b/src/KurrentDB.Client/Core/Common/Constants.cs @@ -41,11 +41,11 @@ public static class Exceptions { public static class Metadata { const string SystemPrefix = "$"; - public const string Type = "type"; - public const string Created = "created"; - public const string ContentType = "content-type"; - public const string SchemaName = $"{SystemPrefix}schema.name"; - public const string SchemaDataFormat = $"{SystemPrefix}schema.data-format"; + public const string Type = "type"; + public const string Created = "created"; + public const string ContentType = "content-type"; + public const string SchemaName = $"{SystemPrefix}schema.name"; + public const string SchemaFormat = $"{SystemPrefix}schema.format"; public static readonly string[] RequiredMetadata = [Type, ContentType]; diff --git a/src/KurrentDB.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs b/src/KurrentDB.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs index 7dcc2fd66..d85d32613 100644 --- a/src/KurrentDB.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs +++ b/src/KurrentDB.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs @@ -27,30 +27,6 @@ public static async ValueTask TraceClientOperation( } } - public static async ValueTask TraceMultiStreamAppend( - this ActivitySource source, - Func> tracedOperation, - ActivityTagsCollection? tags = null - ) { - using var activity = StartActivity(source, Operations.MultiAppend, ActivityKind.Client, tags, Activity.Current?.Context); - - try { - var result = await tracedOperation().ConfigureAwait(false); - - if (result is MultiAppendFailure { Failures: var failures }) { - activity?.SetStatus(ActivityStatusCode.Error); - failures.ForEach(error => activity?.AddException(error)); - return result; - } - - activity?.StatusOk(); - return result; - } catch (Exception ex) { - activity?.StatusError(ex); - throw; - } - } - public static void TraceSubscriptionEvent( this ActivitySource source, string? subscriptionId, diff --git a/src/KurrentDB.Client/Core/Common/ValueExtensions.cs b/src/KurrentDB.Client/Core/Common/ValueExtensions.cs new file mode 100644 index 000000000..f03bfdaa4 --- /dev/null +++ b/src/KurrentDB.Client/Core/Common/ValueExtensions.cs @@ -0,0 +1,274 @@ +using System.Globalization; +using System.Runtime.CompilerServices; +using Google.Protobuf.WellKnownTypes; +using Enum = System.Enum; + +namespace KurrentDB.Client; + +public static class ValueExtensions { + // Epsilon tolerance for floating point comparisons to handle precision issues + const double IntegerEpsilon = 1e-10; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGetString(this Value value, out string? result) { + if (value.KindCase == Value.KindOneofCase.StringValue) { + result = value.StringValue; + return true; + } + + result = null; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGetBoolean(this Value value, out bool result) { + switch (value.KindCase) { + case Value.KindOneofCase.BoolValue: + result = value.BoolValue; + return true; + case Value.KindOneofCase.StringValue when bool.TryParse(value.StringValue, out result): + return true; + default: + result = false; + return false; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGetDouble(this Value value, out double result) { + switch (value.KindCase) { + case Value.KindOneofCase.NumberValue: + result = value.NumberValue; + return true; + case Value.KindOneofCase.StringValue when double.TryParse(value.StringValue, out result): + return true; + default: + result = 0; + return false; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGetInt32(this Value value, out int result) { + switch (value.KindCase) { + case Value.KindOneofCase.NumberValue: { + var d = value.NumberValue; + if (d is >= int.MinValue and <= int.MaxValue) { + var truncated = Math.Truncate(d); + if (Math.Abs(d - truncated) < IntegerEpsilon) { + result = (int)truncated; + return true; + } + } + + result = 0; + return false; + } + case Value.KindOneofCase.StringValue when int.TryParse(value.StringValue, out result): + return true; + default: + result = 0; + return false; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGetInt64(this Value value, out long result) { + switch (value.KindCase) { + case Value.KindOneofCase.NumberValue: { + var d = value.NumberValue; + if (d is >= long.MinValue and <= long.MaxValue) { + var truncated = Math.Truncate(d); + if (Math.Abs(d - truncated) < IntegerEpsilon) { + result = (long)truncated; + return true; + } + } + + result = 0; + return false; + } + case Value.KindOneofCase.StringValue when long.TryParse(value.StringValue, out result): + return true; + default: + result = 0; + return false; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGetSingle(this Value value, out float result) { + switch (value.KindCase) { + case Value.KindOneofCase.NumberValue: { + var d = value.NumberValue; + if (d is >= float.MinValue and <= float.MaxValue) { + result = (float)d; + return true; + } + + result = 0; + return false; + } + case Value.KindOneofCase.StringValue when float.TryParse(value.StringValue, out result): + return true; + default: + result = 0; + return false; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryGetDecimal(this Value value, out decimal result) { + switch (value.KindCase) { + case Value.KindOneofCase.NumberValue: { + var d = value.NumberValue; + if (d is >= (double)decimal.MinValue and <= (double)decimal.MaxValue) { + result = (decimal)d; + return true; + } + + result = 0; + return false; + } + case Value.KindOneofCase.StringValue when decimal.TryParse(value.StringValue, out result): + return true; + default: + result = 0; + return false; + } + } + + public static bool TryGetGuid(this Value value, out Guid result) { + if (value.KindCase == Value.KindOneofCase.StringValue) + return Guid.TryParse(value.StringValue, out result); + + result = Guid.Empty; + return false; + } + + public static bool TryGetDateTime(this Value value, out DateTime result) { + if (value.KindCase == Value.KindOneofCase.StringValue) + return DateTime.TryParse(value.StringValue, out result); + + result = default; + return false; + } + + public static bool TryGetDateTimeOffset(this Value value, out DateTimeOffset result) { + if (value.KindCase == Value.KindOneofCase.StringValue) + return DateTimeOffset.TryParse(value.StringValue, out result); + + result = default; + return false; + } + + public static bool TryGetTimeSpan(this Value value, out TimeSpan result) { + if (value.KindCase == Value.KindOneofCase.StringValue) { + // Try standard TimeSpan parsing first + if (TimeSpan.TryParse(value.StringValue, out result)) + return true; + + // Try ISO 8601 duration format (PT1H30M, P1DT2H, etc.) + if (value.StringValue.Length > 1 && value.StringValue[0] == 'P') { + try { + result = System.Xml.XmlConvert.ToTimeSpan(value.StringValue); + return true; + } catch { + // Fall through to false + } + } + } + + result = TimeSpan.Zero; + return false; + } + + public static bool TryGetEnum(this Value value, out T result) where T : struct, Enum { + if (value.KindCase == Value.KindOneofCase.StringValue) + return Enum.TryParse(value.StringValue, ignoreCase: true, out result); + + result = default; + return false; + } + + static string GetDisplayValue(Value value) => value.KindCase switch { + Value.KindOneofCase.StringValue => $"'{value.StringValue}'", + Value.KindOneofCase.NumberValue => value.NumberValue.ToString(CultureInfo.InvariantCulture), + Value.KindOneofCase.BoolValue => value.BoolValue.ToString(), + Value.KindOneofCase.NullValue => "null", + Value.KindOneofCase.None => "none", + _ => $"<{value.KindCase}>" + }; + + public static string GetString(this Value value) { + if (TryGetString(value, out var result) && result != null) + return result; + throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to String"); + } + + public static bool GetBoolean(this Value value) { + return TryGetBoolean(value, out var result) + ? result + : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to Boolean"); + } + + public static double GetDouble(this Value value) { + return TryGetDouble(value, out var result) + ? result + : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to Double"); + } + + public static int GetInt32(this Value value) { + return TryGetInt32(value, out var result) + ? result + : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to Int32"); + } + + public static long GetInt64(this Value value) { + return TryGetInt64(value, out var result) + ? result + : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to Int64"); + } + + public static float GetSingle(this Value value) { + return TryGetSingle(value, out var result) + ? result + : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to Single"); + } + + public static decimal GetDecimal(this Value value) { + return TryGetDecimal(value, out var result) + ? result + : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to Decimal"); + } + + public static Guid GetGuid(this Value value) { + return TryGetGuid(value, out var result) + ? result + : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to Guid"); + } + + public static DateTime GetDateTime(this Value value) { + return TryGetDateTime(value, out var result) + ? result + : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to DateTime"); + } + + public static DateTimeOffset GetDateTimeOffset(this Value value) { + return TryGetDateTimeOffset(value, out var result) + ? result + : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to DateTimeOffset"); + } + + public static TimeSpan GetTimeSpan(this Value value) { + return TryGetTimeSpan(value, out var result) + ? result + : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to TimeSpan"); + } + + public static T GetEnum(this Value value) where T : struct, Enum { + return TryGetEnum(value, out var result) + ? result + : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to {typeof(T).Name}"); + } +} diff --git a/src/KurrentDB.Client/Core/DynamicValueMapper.cs b/src/KurrentDB.Client/Core/DynamicValueMapper.cs deleted file mode 100644 index ab3ad6b72..000000000 --- a/src/KurrentDB.Client/Core/DynamicValueMapper.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Google.Protobuf; -using Google.Protobuf.Collections; -using Google.Protobuf.WellKnownTypes; -using JetBrains.Annotations; -using KurrentDB.Protocol; - -namespace KurrentDB.Client; - -[PublicAPI] -static class DynamicValueMapper { - public static MapField MapToDynamicMapField(this Dictionary source) => - source.Aggregate( - new MapField(), - (seed, entry) => { - seed.Add(entry.Key, MapToDynamicValue(entry.Value)); - return seed; - } - ); - - public static DynamicValue MapToDynamicValue(this object? source) { - return source switch { - null => new() { NullValue = NullValue.NullValue }, - string x => new() { StringValue = x }, - bool x => new() { BooleanValue = x }, - int x => new() { Int32Value = x }, - long x => new() { Int64Value = x }, - float x => new() { FloatValue = x }, - double x => new() { DoubleValue = x }, - - DateTime x => new() { TimestampValue = x.ToTimestamp() }, - DateTimeOffset x => new() { TimestampValue = x.ToTimestamp() }, - TimeSpan x => new() { DurationValue = x.ToDuration() }, - - byte[] x => new() { BytesValue = ByteString.CopyFrom(x) }, - ReadOnlyMemory x => new() { BytesValue = ByteString.CopyFrom(x.Span) }, - - _ => new() { StringValue = source.ToString() } // any other type is converted to string - }; - } -} diff --git a/src/KurrentDB.Client/Core/Exceptions/WrongExpectedVersionException.cs b/src/KurrentDB.Client/Core/Exceptions/WrongExpectedVersionException.cs index 71eab9678..0736dc84c 100644 --- a/src/KurrentDB.Client/Core/Exceptions/WrongExpectedVersionException.cs +++ b/src/KurrentDB.Client/Core/Exceptions/WrongExpectedVersionException.cs @@ -1,3 +1,5 @@ +using System; + namespace KurrentDB.Client { /// /// Exception thrown if the expected version specified on an operation @@ -43,11 +45,5 @@ public WrongExpectedVersionException(string streamName, StreamState expectedStre ExpectedVersion = expectedStreamState.ToInt64(); ActualVersion = actualStreamState.ToInt64(); } - - public WrongExpectedVersionException(string streamName, StreamState actualStreamState, Exception? exception = null, string? message = null) : - base(message ?? $"Append failed due to WrongExpectedVersion. Stream: {streamName}, Actual version: {actualStreamState}", exception) { - StreamName = streamName; - ActualStreamState = actualStreamState; - } } } diff --git a/src/KurrentDB.Client/Core/GrpcServerCapabilitiesClient.cs b/src/KurrentDB.Client/Core/GrpcServerCapabilitiesClient.cs index 24a10fd0c..17c156788 100644 --- a/src/KurrentDB.Client/Core/GrpcServerCapabilitiesClient.cs +++ b/src/KurrentDB.Client/Core/GrpcServerCapabilitiesClient.cs @@ -53,7 +53,7 @@ public async Task GetAsync( case ("event_store.client.persistent_subscriptions.persistentsubscriptions", "list"): supportsPersistentSubscriptionsList = true; continue; - case ("kurrentdb.protocol.v2.streamsservice", "multistreamappend"): + case ("kurrentdb.protocol.v2.streams.streamsservice", "appendsession"): supportsMultiStreamAppend = true; continue; } diff --git a/src/KurrentDB.Client/Core/ValueMapper.cs b/src/KurrentDB.Client/Core/ValueMapper.cs new file mode 100644 index 000000000..82ed1280f --- /dev/null +++ b/src/KurrentDB.Client/Core/ValueMapper.cs @@ -0,0 +1,44 @@ +using Google.Protobuf.Collections; +using Google.Protobuf.WellKnownTypes; +using JetBrains.Annotations; + +namespace KurrentDB.Client; + +[PublicAPI] +static class ValueMapper { + public static MapField MapToMapValue(this Dictionary source) => + source.Aggregate( + new MapField(), + (seed, entry) => { + seed.Add(entry.Key, MapToValue(entry.Value)); + return seed; + } + ); + + public static Value MapToValue(this object? source) { + return source switch { + null => new() { NullValue = NullValue.NullValue }, + string x => new() { StringValue = x }, + bool x => new() { BoolValue = x }, + int x => new() { NumberValue = x }, + long x => new() { NumberValue = x }, + float x => new() { NumberValue = x }, + double x => new() { NumberValue = x }, + + DateTime x => new() { StringValue = x.ToUniversalTime().ToString("O") }, + DateTimeOffset x => new() { StringValue = x.ToUniversalTime().ToString("O") }, + TimeSpan x => new() { StringValue = x.ToString() }, + + byte[] x => new() { StringValue = Convert.ToBase64String(x) }, + ReadOnlyMemory x => new() { +#if NET48 + StringValue = Convert.ToBase64String(x.ToArray()) +#else + StringValue = Convert.ToBase64String(x.Span) +#endif + }, + + _ => new() { StringValue = source.ToString() } // any other type is converted to string + }; + } +} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/code.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/code.proto deleted file mode 100644 index 98ae0ac18..000000000 --- a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/code.proto +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright 2020 Google LLC -// -// 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. - -syntax = "proto3"; - -package google.rpc; - -option go_package = "google.golang.org/genproto/googleapis/rpc/code;code"; -option java_multiple_files = true; -option java_outer_classname = "CodeProto"; -option java_package = "com.google.rpc"; -option objc_class_prefix = "RPC"; - -// The canonical error codes for gRPC APIs. -// -// -// Sometimes multiple error codes may apply. Services should return -// the most specific error code that applies. For example, prefer -// `OUT_OF_RANGE` over `FAILED_PRECONDITION` if both codes apply. -// Similarly prefer `NOT_FOUND` or `ALREADY_EXISTS` over `FAILED_PRECONDITION`. -enum Code { - // Not an error; returned on success - // - // HTTP Mapping: 200 OK - OK = 0; - - // The operation was cancelled, typically by the caller. - // - // HTTP Mapping: 499 Client Closed Request - CANCELLED = 1; - - // Unknown error. For example, this error may be returned when - // a `Status` value received from another address space belongs to - // an error space that is not known in this address space. Also - // errors raised by APIs that do not return enough error information - // may be converted to this error. - // - // HTTP Mapping: 500 Internal Server Error - UNKNOWN = 2; - - // The client specified an invalid argument. Note that this differs - // from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments - // that are problematic regardless of the state of the system - // (e.g., a malformed file name). - // - // HTTP Mapping: 400 Bad Request - INVALID_ARGUMENT = 3; - - // The deadline expired before the operation could complete. For operations - // that change the state of the system, this error may be returned - // even if the operation has completed successfully. For example, a - // successful response from a server could have been delayed long - // enough for the deadline to expire. - // - // HTTP Mapping: 504 Gateway Timeout - DEADLINE_EXCEEDED = 4; - - // Some requested entity (e.g., file or directory) was not found. - // - // Note to server developers: if a request is denied for an entire class - // of users, such as gradual feature rollout or undocumented whitelist, - // `NOT_FOUND` may be used. If a request is denied for some users within - // a class of users, such as user-based access control, `PERMISSION_DENIED` - // must be used. - // - // HTTP Mapping: 404 Not Found - NOT_FOUND = 5; - - // The entity that a client attempted to create (e.g., file or directory) - // already exists. - // - // HTTP Mapping: 409 Conflict - ALREADY_EXISTS = 6; - - // The caller does not have permission to execute the specified - // operation. `PERMISSION_DENIED` must not be used for rejections - // caused by exhausting some resource (use `RESOURCE_EXHAUSTED` - // instead for those errors). `PERMISSION_DENIED` must not be - // used if the caller can not be identified (use `UNAUTHENTICATED` - // instead for those errors). This error code does not imply the - // request is valid or the requested entity exists or satisfies - // other pre-conditions. - // - // HTTP Mapping: 403 Forbidden - PERMISSION_DENIED = 7; - - // The request does not have valid authentication credentials for the - // operation. - // - // HTTP Mapping: 401 Unauthorized - UNAUTHENTICATED = 16; - - // Some resource has been exhausted, perhaps a per-user quota, or - // perhaps the entire file system is out of space. - // - // HTTP Mapping: 429 Too Many Requests - RESOURCE_EXHAUSTED = 8; - - // The operation was rejected because the system is not in a state - // required for the operation's execution. For example, the directory - // to be deleted is non-empty, an rmdir operation is applied to - // a non-directory, etc. - // - // Service implementors can use the following guidelines to decide - // between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`: - // (a) Use `UNAVAILABLE` if the client can retry just the failing call. - // (b) Use `ABORTED` if the client should retry at a higher level - // (e.g., when a client-specified test-and-set fails, indicating the - // client should restart a read-modify-write sequence). - // (c) Use `FAILED_PRECONDITION` if the client should not retry until - // the system state has been explicitly fixed. E.g., if an "rmdir" - // fails because the directory is non-empty, `FAILED_PRECONDITION` - // should be returned since the client should not retry unless - // the files are deleted from the directory. - // - // HTTP Mapping: 400 Bad Request - FAILED_PRECONDITION = 9; - - // The operation was aborted, typically due to a concurrency issue such as - // a sequencer check failure or transaction abort. - // - // See the guidelines above for deciding between `FAILED_PRECONDITION`, - // `ABORTED`, and `UNAVAILABLE`. - // - // HTTP Mapping: 409 Conflict - ABORTED = 10; - - // The operation was attempted past the valid range. E.g., seeking or - // reading past end-of-file. - // - // Unlike `INVALID_ARGUMENT`, this error indicates a problem that may - // be fixed if the system state changes. For example, a 32-bit file - // system will generate `INVALID_ARGUMENT` if asked to read at an - // offset that is not in the range [0,2^32-1], but it will generate - // `OUT_OF_RANGE` if asked to read from an offset past the current - // file size. - // - // There is a fair bit of overlap between `FAILED_PRECONDITION` and - // `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific - // error) when it applies so that callers who are iterating through - // a space can easily look for an `OUT_OF_RANGE` error to detect when - // they are done. - // - // HTTP Mapping: 400 Bad Request - OUT_OF_RANGE = 11; - - // The operation is not implemented or is not supported/enabled in this - // service. - // - // HTTP Mapping: 501 Not Implemented - UNIMPLEMENTED = 12; - - // Internal errors. This means that some invariants expected by the - // underlying system have been broken. This error code is reserved - // for serious errors. - // - // HTTP Mapping: 500 Internal Server Error - INTERNAL = 13; - - // The service is currently unavailable. This is most likely a - // transient condition, which can be corrected by retrying with - // a backoff. Note that it is not always safe to retry - // non-idempotent operations. - // - // See the guidelines above for deciding between `FAILED_PRECONDITION`, - // `ABORTED`, and `UNAVAILABLE`. - // - // HTTP Mapping: 503 Service Unavailable - UNAVAILABLE = 14; - - // Unrecoverable data loss or corruption. - // - // HTTP Mapping: 500 Internal Server Error - DATA_LOSS = 15; -} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/status.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/status.proto deleted file mode 100644 index 65eced268..000000000 --- a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/status.proto +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2020 Google LLC -// -// 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. - -syntax = "proto3"; - -package google.rpc; - -import "google/protobuf/any.proto"; -import "code.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/rpc/status;status"; -option java_multiple_files = true; -option java_outer_classname = "StatusProto"; -option java_package = "com.google.rpc"; -option objc_class_prefix = "RPC"; - -// The `Status` type defines a logical error model that is suitable for -// different programming environments, including REST APIs and RPC APIs. It is -// used by [gRPC](https://github.com/grpc). Each `Status` message contains -// three pieces of data: error code, error message, and error details. -// -// You can find out more about this error model and how to work with it in the -// [API Design Guide](https://cloud.google.com/apis/design/errors). -message Status { - // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. - google.rpc.Code code = 1; - - // A developer-facing error message, which should be in English. Any - // user-facing error message should be localized and sent in the - // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. - string message = 2; - - // A list of messages that carry the error details. There is a common set of - // message types for APIs to use. - google.protobuf.Any details = 3; -} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/streams.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/streams.proto index 3e2053f8b..0f51b23c5 100644 --- a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/streams.proto +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/streams.proto @@ -7,7 +7,7 @@ option csharp_namespace = "KurrentDB.Protocol.Streams.V1"; option java_package = "com.eventstore.dbclient.proto.streams"; import "shared.proto"; -import "status.proto"; +import "google/rpc/status.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/core.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/core.proto deleted file mode 100644 index d22daf893..000000000 --- a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/core.proto +++ /dev/null @@ -1,129 +0,0 @@ -syntax = "proto3"; - -package kurrentdb.protocol; - -option csharp_namespace = "KurrentDB.Protocol"; -option java_package = "io.kurrentdb.protocol"; -option java_multiple_files = true; - -import "google/protobuf/timestamp.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "google/protobuf/descriptor.proto"; - -//=================================================================== -// Dynamic values -//=================================================================== - -// Represents a list of dynamically typed values. -message DynamicValueList { - // Repeated property of dynamically typed values. - repeated DynamicValue values = 1; -} - -// Represents a map of dynamically typed values. -message DynamicValueMap { - // A map of string keys to dynamically typed values. - map values = 1; -} - -// Represents a dynamic value -message DynamicValue { - oneof kind { - // Represents a null value. - google.protobuf.NullValue null_value = 1; - // Represents a 32-bit signed integer value. - sint32 int32_value = 2; - // Represents a 64-bit signed integer value. - sint64 int64_value = 3; - // Represents a byte array value. - bytes bytes_value = 4; - // Represents a 64-bit double-precision floating-point value. - double double_value = 5; - // Represents a 32-bit single-precision floating-point value - float float_value = 6; - // Represents a string value. - string string_value = 7; - // Represents a boolean value. - bool boolean_value = 8; - // Represents a timestamp value. - google.protobuf.Timestamp timestamp_value = 9; - // Represents a duration value. - google.protobuf.Duration duration_value = 10; - } -} - -//=================================================================== -// Error Annotations -//=================================================================== - -message ErrorAnnotations { - // Identifies the error condition. - string code = 1; - - // Severity of the error. - Severity severity = 2; - - // Human-readable message that describes the error condition. - optional string message = 3; - - enum Severity { - // The error is recoverable, the operation failed but the session can continue. - RECOVERABLE = 0; - // The error is fatal and the session should be terminated. - FATAL = 1; - // The error is retriable, the operation failed but can be retried. - RETRIABLE = 2; - } -} - -// Extend the MessageOptions to include error information. -extend google.protobuf.MessageOptions { - // Provides additional information about the error condition. - optional ErrorAnnotations error_info = 50000; -} - -//=================================================================== -// Error Details -//=================================================================== - -// Provides detailed information about specific error conditions. -message CoreErrorDetails { - // When the user does not have sufficient permissions to perform the - // operation. - message AccessDenied { - option (error_info) = { - code : "ACCESS_DENIED", - severity : RECOVERABLE, - message : "The user does not have sufficient permissions to perform the operation. Please check your permissions and try again." - }; - } - - // When the user does not have sufficient permissions to perform the - // operation. - message DeadlineExceeded { - option (error_info) = { - code : "DEADLINE_EXCEEDED", - severity : RECOVERABLE, - message : "The operation has timed out and exceeded the deadline. Please try again later." - }; - } - - // When the user is not authenticated. - message NotAuthenticated { - option (error_info) = { - code : "NOT_AUTHENTICATED", - severity : RECOVERABLE, - message : "The user is not authenticated. Please provide valid authentication credentials." - }; - } - - // When the user is not found. - message UserNotFound { - option (error_info) = { - code : "USER_NOT_FOUND", - severity : RECOVERABLE, - message : "The specified user was not found. Please check the user ID and try again." - }; - } -} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/errors.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/errors.proto new file mode 100644 index 000000000..f88f09b73 --- /dev/null +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/errors.proto @@ -0,0 +1,147 @@ +// ****************************************************************************************** +// This protocol is UNSTABLE in the sense of being subject to change. +// ****************************************************************************************** + +syntax = "proto3"; + +package kurrentdb.protocol.v2.common.errors; + +option csharp_namespace = "KurrentDB.Protocol.V2.Common.Errors"; + +import "rpc.proto"; + +enum CommonError { + // Default value. This value is not used. + // An error code MUST always be set to a non-zero value. + // If an error code is not explicitly set, it MUST be treated as + // an internal server error (INTERNAL). + UNSPECIFIED = 0; + + ACCESS_DENIED = 1 [(kurrent.rpc.error) = { + status_code: PERMISSION_DENIED, + has_details: true + }]; + + INVALID_REQUEST = 2 [(kurrent.rpc.error) = { + status_code: INVALID_ARGUMENT, + has_details: true + }]; + + NOT_LEADER_NODE = 5 [(kurrent.rpc.error) = { + status_code: FAILED_PRECONDITION, + has_details: true + }]; + + OPERATION_TIMEOUT = 6 [(kurrent.rpc.error) = { + status_code: DEADLINE_EXCEEDED + }]; + + SERVER_NOT_READY = 7 [(kurrent.rpc.error) = { + status_code: UNAVAILABLE + }]; + + SERVER_OVERLOADED = 8 [(kurrent.rpc.error) = { + status_code: UNAVAILABLE + }]; + + SERVER_MALFUNCTION = 9 [(kurrent.rpc.error) = { + status_code: INTERNAL + }]; + +// // The operation was aborted, typically due to a concurrency issue such as a +// // sequencer conflict or transaction abort. +// // This error will only be used when there is no intention to create a dedicated +// // error code for the specific issue, perhaps because the issue is too generic +// // or too transient or temporary in terms of handling. +// OPERATION_ABORTED = 10 [(kurrent.rpc.error) = { +// status_code: ABORTED +// }]; +} + +message AccessDeniedErrorDetails { + // The scope in which access was denied. + // It could represent a resource, a domain, a permission type + // or a "path" that is a combination of these. + // (e.g., "stream:orders", "db:customers:read", etc.) + optional string scope = 1; + + // The username of the user who was denied access. + optional string username = 2; +} + +message InvalidRequestErrorDetails { + // Detailed information about each invalid argument. + repeated FieldViolation violations = 1; + + // Describes a single field violation. + message FieldViolation { + // A path that leads to a field in the request body. The value will be a + // sequence of dot-separated identifiers that identify a protocol buffer + // field. + // + // Consider the following: + // + // message CreateContactRequest { + // message EmailAddress { + // enum Type { + // TYPE_UNSPECIFIED = 0; + // HOME = 1; + // WORK = 2; + // } + // + // optional string email = 1; + // repeated EmailType type = 2; + // } + // + // string full_name = 1; + // repeated EmailAddress email_addresses = 2; + // } + // + // In this example, in proto `field` could take one of the following values: + // + // * `full_name` for a violation in the `full_name` value + // * `email_addresses[1].email` for a violation in the `email` field of the + // first `email_addresses` message + // * `email_addresses[3].type[2]` for a violation in the second `type` + // value in the third `email_addresses` message. + // + // In JSON, the same values are represented as: + // + // * `fullName` for a violation in the `fullName` value + // * `emailAddresses[1].email` for a violation in the `email` field of the + // first `emailAddresses` message + // * `emailAddresses[3].type[2]` for a violation in the second `type` + // value in the third `emailAddresses` message. + string field = 1; + + // A description of why the request element is bad. + string description = 2; + } +} + +message NotLeaderNodeErrorDetails { + // The host of the current leader node + string host = 1; + + // The port of the current leader node + int32 port = 2; + + // The instance ID of the current leader node + optional string node_id = 3; +} + +message RetryInfoErrorDetails { + // The duration in milliseconds after which the client can retry the operation. + int32 retry_delay_ms = 1; +} + +message NodeInfoErrorDetails { + // The host of the node + string host = 1; + + // The port of the node + int32 port = 2; + + // The instance ID of the node + optional string node_id = 3; +} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/rpc.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/rpc.proto new file mode 100644 index 000000000..21382b950 --- /dev/null +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/rpc.proto @@ -0,0 +1,91 @@ +// ****************************************************************************************** +// This protocol is UNSTABLE in the sense of being subject to change. +// ****************************************************************************************** + +syntax = "proto3"; + +package kurrent.rpc; + +option csharp_namespace = "Kurrent.Rpc"; + +import "google/protobuf/descriptor.proto"; +import "google/rpc/code.proto"; + +// ErrorMetadata provides actionable information for error enum values to enable automated +// code generation, documentation, and consistent error handling across the Kurrent platform. +// +// It was modeled to support a single details type per error code to simplify code generation and +// validation. If multiple detail types are needed for a single error code, consider defining +// separate error codes for each detail type. Or, use a union type (oneof) in the detail message +// to encapsulate multiple detail variants within a single detail message. +// +// More however DebugInfo and RetryInfo can and should be added to any error regardless of +// this setting, when applicable. +// +// This annotation is applied to enum values using the google.protobuf.EnumValueOptions +// extension mechanism. It enables: +// - Automatic gRPC status code mapping +// - Code generation for error handling utilities +// - Documentation generation +// - Type-safe error detail validation +// +// Usage Example: +// enum StreamErrorCode { +// REVISION_CONFLICT = 5 [(kurrent.rpc.error) = { +// status_code: FAILED_PRECONDITION, +// has_details: true +// }]; +// } +// +// See individual field documentation for conventions and defaults. +message ErrorMetadata { + // Maps the error to a standard gRPC status code for transport-level compatibility. + // This field is REQUIRED for every error annotation. + // + // Use standard gRPC status codes from `google.rpc.code`. + // + // Code generators use this to: + // - Map errors to gRPC status codes automatically + // - Generate HTTP status code mappings + // - Create transport-agnostic error handling + google.rpc.Code status_code = 1; + + // Indicates whether this error supports rich, typed detail messages. + // Defaults to false (simple message string only). + // The message type name must be derived from the enum name by convention. + // Mask: {EnumValue}ErrorDetails + // + // Examples: + // ACCESS_DENIED -> "AccessDeniedErrorDetails" + // SERVER_NOT_READY -> "ServerNotReadyErrorDetails" + // + // Code generators use the message type name to: + // - Validate that the detail message matches the expected type + // - Generate type-safe error handling code + // - Create accurate documentation + bool has_details = 2; +} + +// Extend EnumValueOptions to include error information for enum values +extend google.protobuf.EnumValueOptions { + // Provides additional information about error conditions for automated + // code generation and documentation. + optional ErrorMetadata error = 50000; +} + +// The top-level error message that must be returned by any service or operation +// in the Kurrent platform. +message RequestErrorInfo { + // The code must match one of the defined enum error codes from the module + // where the error originated from. + // A machine-readable error code that indicates the specific error condition. + // This should be at most 63 characters and match a regular expression of + // `[A-Z][A-Z0-9_]+[A-Z0-9]`, which represents UPPER_SNAKE_CASE. + // By convention, it will be generated from the enum value name if not + // explicitly specified. + // Conventions: + // - Prefix with the service name or domain to avoid collisions + // - Use UPPER_SNAKE_CASE with only letters, numbers, and underscores + // - Avoid redundant information (e.g., do not include "ERROR" suffix) + string code = 1; +} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/error_details.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/error_details.proto deleted file mode 100644 index 4c38af4f3..000000000 --- a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/error_details.proto +++ /dev/null @@ -1,88 +0,0 @@ -syntax = "proto3"; - -package kurrentdb.protocol.v2; - -option csharp_namespace = "KurrentDB.Protocol.Streams.V2"; -option java_package = "io.kurrentdb.protocol.streams.v2"; -option java_multiple_files = true; - -import "google/protobuf/timestamp.proto"; -import "core.proto"; - -message StreamsErrorDetails { - // When the stream has been deleted. - message StreamDeleted { - option (error_info) = { - code : "STREAM_DELETED", - severity : RECOVERABLE, - message : "The stream has been soft deleted. It will not be visible in the stream list, until it is restored by appending to it again." - }; - - // The name of the stream that was deleted. - optional string stream = 1; - - // The time when the stream was deleted. - google.protobuf.Timestamp deleted_at = 2; - } - - // When the stream has been tombstoned. - message StreamTombstoned { - option (error_info) = { - code : "STREAM_TOMBSTONED", - severity : FATAL, - message : "The stream has been tombstoned. It has been permanently removed from the system and cannot be restored." - }; - - // The name of the stream that was tombstoned. - optional string stream = 1; - - // The time when the stream was tombstoned. - google.protobuf.Timestamp tombstoned_at = 2; - } - - // When the stream is not found. - message StreamNotFound { - option (error_info) = { - code : "STREAM_NOT_FOUND", - severity : RECOVERABLE, - message : "The specified stream was not found. Please check the stream name and try again." - }; - - // The name of the stream that was not found. - optional string stream = 1; - } - - // When the expected revision of the stream does not match the actual - // revision. - message StreamRevisionConflict { - option (error_info) = { - code : "REVISION_CONFLICT", - severity : RECOVERABLE, - message : "The actual stream revision does not match the expected revision." - }; - - // The actual revision of the stream. - int64 stream_revision = 1; - } - - // When the transaction exceeds the maximum size allowed - // (its bigger than the configured chunk size). - message TransactionMaxSizeExceeded { - option (error_info) = { - code : "TRANSACTION_MAX_SIZE_EXCEEDED", - severity : FATAL, - message : "The transaction exceeds the maximum size allowed." - }; - - // The maximum allowed size of the transaction. - uint32 max_size = 1; - } - - message LogPositionNotFound { - option (error_info) = { - code : "LOG_POSITION_NOT_FOUND", - severity : RECOVERABLE, - message : "The specified log position was not found." - }; - } -} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/errors.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/errors.proto new file mode 100644 index 000000000..260bf8f59 --- /dev/null +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/errors.proto @@ -0,0 +1,127 @@ +// ****************************************************************************************** +// This protocol is UNSTABLE in the sense of being subject to change. +// ****************************************************************************************** + +syntax = "proto3"; + +package kurrentdb.protocol.v2.streams.errors; + +option csharp_namespace = "KurrentDB.Protocol.V2.Streams.Errors"; + +import "rpc.proto"; + +enum StreamsError { + // Default value. This value is not used. + // An error code MUST always be set to a non-zero value. + // If an error code is not explicitly set, it MUST be treated as + // an internal server error (INTERNAL). + UNSPECIFIED = 0; + + // The stream was not found. + // This is recoverable by the client by creating the stream first. + STREAM_NOT_FOUND = 1 [(kurrent.rpc.error) = { + status_code: NOT_FOUND, + has_details: true, + }]; + + // The stream already exists. + // This is recoverable by the client by using the existing stream. + STREAM_ALREADY_EXISTS = 2 [(kurrent.rpc.error) = { + status_code: ALREADY_EXISTS, + has_details: true + }]; + + // The stream has been soft deleted. + // It will not be visible in the stream list, until it is restored by appending to it again. + STREAM_DELETED = 3 [(kurrent.rpc.error) = { + status_code: FAILED_PRECONDITION, + has_details: true + }]; + + // The stream has been tombstoned. + // It has been permanently removed from the system and cannot be restored. + STREAM_TOMBSTONED = 4 [(kurrent.rpc.error) = { + status_code: FAILED_PRECONDITION, + has_details: true + }]; + + // The expected revision of the stream does not match the actual revision. + // This is recoverable by the client by fetching the current revision and retrying. + STREAM_REVISION_CONFLICT = 5 [(kurrent.rpc.error) = { + status_code: FAILED_PRECONDITION, + has_details: true + }]; + + // The size of a record being appended exceeds the maximum allowed size. + // It is recoverable by the client by sending a smaller record. + APPEND_RECORD_SIZE_EXCEEDED = 6 [(kurrent.rpc.error) = { + status_code: INVALID_ARGUMENT, + has_details: true + }]; + + // When the transaction exceeds the maximum size allowed (max chunk size). + // It is recoverable by the client by sending a smaller transaction. + APPEND_TRANSACTION_SIZE_EXCEEDED = 7 [(kurrent.rpc.error) = { + status_code: ABORTED, + has_details: true + }]; + + // The stream is already in an append session. + // Appending to the same stream multiple times is currently not supported. + STREAM_ALREADY_IN_APPEND_SESSION = 8 [(kurrent.rpc.error) = { + status_code: ABORTED, + has_details: true + }]; +} + +message StreamNotFoundErrorDetails { + // The name of the stream that was not found. + string stream = 1; +} + +message StreamAlreadyExistsErrorDetails { + // The name of the stream that already exists. + string stream = 1; +} + +message StreamDeletedErrorDetails { + // The name of the stream that was deleted. + string stream = 1; +} + +message StreamTombstonedErrorDetails { + // The name of the stream that was tombstoned. + string stream = 1; +} + +message StreamRevisionConflictErrorDetails { + // The name of the stream that had a revision conflict. + string stream = 1; + // The actual revision of the stream. + int64 expected_revision = 2; + // The actual revision of the stream. + int64 actual_revision = 3; +} + +message AppendRecordSizeExceededErrorDetails { + // The name of the stream where the append was attempted. + string stream = 1; + // The identifier of the offending and oversized record. + string record_id = 2; + // The size of the huge record in bytes. + int32 size = 3; + // The maximum allowed size of a single record that can be appended in bytes. + int32 max_size = 4; +} + +message AppendTransactionSizeExceededErrorDetails { + // The size of the huge transaction in bytes. + int32 size = 1; + // The maximum allowed size of the append transaction in bytes. + int32 max_size = 2; +} + +message StreamAlreadyInAppendSessionErrorDetails { + // The name of the stream that is already in an append session. + string stream = 1; +} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto index 9dbc08222..99803559e 100644 --- a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto @@ -1,91 +1,39 @@ -syntax = "proto3"; +// ****************************************************************************************** +// This protocol is UNSTABLE in the sense of being subject to change. +// ****************************************************************************************** -package kurrentdb.protocol.v2; +syntax = "proto3"; -option csharp_namespace = "KurrentDB.Protocol.Streams.V2"; -option java_package = "io.kurrentdb.protocol.streams.v2"; -option java_multiple_files = true; +package kurrentdb.protocol.v2.streams; -import "google/protobuf/timestamp.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/descriptor.proto"; +option csharp_namespace = "KurrentDB.Protocol.V2.Streams"; -import "streams/error_details.proto"; -import "core.proto"; +import "google/protobuf/struct.proto"; service StreamsService { - // Executes an atomic operation to append records to multiple streams. - // This transactional method ensures that all appends either succeed - // completely, or are entirely rolled back, thereby maintaining strict data - // consistency across all involved streams. - rpc MultiStreamAppend(MultiStreamAppendRequest) returns (MultiStreamAppendResponse); - - // Streaming version of MultiStreamAppend that allows clients to send multiple - // append requests over a single connection. When the stream completes, all - // records are appended transactionally (all succeed or fail together). + // // Executes an atomic operation to append records to multiple streams. + // // This transactional method ensures that all appends either succeed + // // completely, or are entirely rolled back, thereby maintaining strict + // // data consistency across all involved streams. + // rpc Append(AppendRequest) returns (AppendResponse); + + // Streaming version of Append that allows clients to send multiple + // append requests continuously. Once completed, all records are + // appended transactionally (all succeed or fail together). // Provides improved efficiency for high-throughput scenarios while // maintaining the same transactional guarantees. - rpc MultiStreamAppendSession(stream AppendStreamRequest) returns (MultiStreamAppendResponse); - -// // Appends records to a specific stream. -// rpc AppendStream(AppendStreamRequest) returns (AppendStreamResponse); - -// // Append batches of records to a stream continuously, while guaranteeing pipelined -// // requests are processed in order. If any request fails, the session is terminated. -// rpc AppendStreamSession(stream AppendStreamRequest) returns (stream AppendStreamResponse); - -// // Retrieve a batch of records -// rpc ReadStream(ReadRequest) returns (ReadResponse); - -// // Retrieve batches of records continuously. -// rpc ReadSession(ReadRequest) returns (stream ReadResponse); + rpc AppendSession(stream AppendRequest) returns (AppendSessionResponse); } -//=================================================================== -// Append Operations -//=================================================================== +message AppendSessionResponse { + repeated AppendResponse output = 1; -// Record to be appended to a stream. -message AppendRecord { - // Universally Unique identifier for the record. - // If not provided, the server will generate a new one. - optional string record_id = 1; - -// // The name of the stream to append the record to. -// optional string stream = 6; -// -// // The name of the schema in the registry that defines the structure of the record. -// string schema_name = 4; -// -// // The format of the data in the record. -// SchemaDataFormat data_format = 5; - - // A collection of properties providing additional information about the - // record. This can include user-defined metadata or system properties. - // System properties are prefixed with "$." to avoid conflicts with user-defined properties. - // For example, "$schema.name" or "$schema.data-format". - map properties = 2; - - // The actual data payload of the record, stored as bytes. - bytes data = 3; -} - -// Constants that match the expected state of a stream during an -// append operation. It can be used to specify whether the stream should exist, -// not exist, or can be in any state. -enum ExpectedRevisionConstants { - // The stream should exist and the expected revision should match the current - EXPECTED_REVISION_CONSTANTS_SINGLE_EVENT = 0; - // It is not important whether the stream exists or not. - EXPECTED_REVISION_CONSTANTS_ANY = -2; - // The stream should not exist. If it does, the append will fail. - EXPECTED_REVISION_CONSTANTS_NO_STREAM = -1; - // The stream should exist - EXPECTED_REVISION_CONSTANTS_EXISTS = -4; + // The position of the last appended record in the transaction. + int64 position = 2; } // Represents the input for appending records to a specific stream. -message AppendStreamRequest { +message AppendRequest { // The name of the stream to append records to. string stream = 1; // The records to append to the stream. @@ -98,382 +46,75 @@ message AppendStreamRequest { optional sint64 expected_revision = 3; } -// Success represents the successful outcome of an append operation. -message AppendStreamSuccess { +// Represents the outcome of an append operation. +message AppendResponse { // The name of the stream to which records were appended. string stream = 1; - // The position of the last appended record in the stream. - int64 position = 2; // The expected revision of the stream after the append operation. - int64 stream_revision = 3; + int64 stream_revision = 2; } -// Failure represents the detailed error information when an append operation fails. -message AppendStreamFailure { - // The name of the stream to which records were appended. - string stream = 1; - - // The error details - oneof error { - // Failed because the actual stream revision didn't match the expected revision. - StreamsErrorDetails.StreamRevisionConflict stream_revision_conflict = 2; - // Failed because the client lacks sufficient permissions. - CoreErrorDetails.AccessDenied access_denied = 3; - // Failed because the target stream has been deleted. - StreamsErrorDetails.StreamDeleted stream_deleted = 4; - // Failed because the transaction exceeded the maximum size allowed - StreamsErrorDetails.TransactionMaxSizeExceeded transaction_max_size_exceeded = 6; - } +// Represents the data format of the schema. +enum SchemaFormat { + // Default value, should not be used. + SCHEMA_FORMAT_UNSPECIFIED = 0; + SCHEMA_FORMAT_JSON = 1; + SCHEMA_FORMAT_PROTOBUF = 2; + SCHEMA_FORMAT_AVRO = 3; + SCHEMA_FORMAT_BYTES = 4; } -// AppendStreamResponse represents the output of appending records to a specific -// stream. -message AppendStreamResponse { - // The result of the append operation. - oneof result { - // Success represents the successful outcome of an append operation. - AppendStreamSuccess success = 1; - // Failure represents the details of a failed append operation. - AppendStreamFailure failure = 2; - } -} +message SchemaInfo { + // The format of the schema that the record conforms to. + SchemaFormat format = 1; -// MultiStreamAppendRequest represents a request to append records to multiple streams. -message MultiStreamAppendRequest { - // A list of AppendStreamInput messages, each representing a stream to which records should be appended. - repeated AppendStreamRequest input = 1; + // The name of the schema that the record conforms to. + string name = 2; + + // The identifier of the specific version of the schema that the record payload + // conforms to. This should match a registered schema version in the system. + // Not necessary when not enforcing schema validation. + optional string id = 3; } -// Response from the MultiStreamAppend operation. -message MultiStreamAppendResponse { - oneof result { - // Success represents the successful outcome of a multi-stream append operation. - Success success = 1; - // Failure represents the details of a failed multi-stream append operation. - Failure failure = 2; - } +// Record to be appended to a stream. +message AppendRecord { + // Universally Unique identifier for the record. Must be a guid. + // If not provided, the server will generate a new one. + optional string record_id = 1; + + // The timestamp of when the record was created, represented as + // milliseconds since the Unix epoch. This is primarily for + // informational purposes and does not affect the ordering of records + // within the stream, which is determined by the server. + // If not provided, the server will assign it upon receipt. + optional int64 timestamp = 2; + + // A collection of properties providing additional information about the + // record. This can include user-defined metadata or system properties. + // System properties are uniquely identified by the "$." prefix. + map properties = 3; - message Success { - repeated AppendStreamSuccess output = 1; - } + // Information about the schema that the record payload conforms to. + SchemaInfo schema = 4; - message Failure { - repeated AppendStreamFailure output = 1; - } + // The actual data payload of the record. + bytes data = 5; } -////=================================================================== -//// Read Operations -////=================================================================== -// -//// The scope of the read filter determines where the filter will be applied. -//enum ReadFilterScope { -// READ_FILTER_SCOPE_UNSPECIFIED = 0; -// // The filter will be applied to the record stream name -// READ_FILTER_SCOPE_STREAM = 1; -// // The filter will be applied to the record schema name -// READ_FILTER_SCOPE_SCHEMA_NAME = 2; -// // The filter will be applied to the properties of the record -// READ_FILTER_SCOPE_PROPERTIES = 3; -// // The filter will be applied to all the record properties -// // including the stream and schema name -// READ_FILTER_SCOPE_RECORD = 4; -//} -// -//// The filter to apply when reading records from the database -//// The combination of stream scope and literal expression indicates a direct stream name match, -//// while a regex expression indicates a pattern match across multiple streams. -//message ReadFilter { -// // The scope of the filter. -// ReadFilterScope scope = 1; -// // The expression can be a regular expression or a literal value. -// // If it starts with "~" it will be considered a regex. -// string expression = 2; -// -// // // The optional name of the record property to filter on. -// // optional string property_name = 3; -// -// // The optional property names to filter on. -// repeated string property_names = 4; -//} -// -//// Record retrieved from the database. -//message Record { -// // The unique identifier of the record in the database. -// string record_id = 1; -// // The position of the record in the database. -// int64 position = 5; -// // The actual data payload of the record, stored as bytes. -// bytes data = 2; -// // Additional information about the record. -// map properties = 3; -// // When the record was created. -// google.protobuf.Timestamp timestamp = 4; -// // The stream to which the record belongs. -// optional string stream = 6; -// // The revision of the stream created when the record was appended. -// optional int64 stream_revision = 7; -//} -// -//// The direction in which to read records from the database (forwards or backwards). -//enum ReadDirection { -// READ_DIRECTION_FORWARDS = 0; -// READ_DIRECTION_BACKWARDS = 1; -//} -// -//// The position from which to start reading records. -//// This can be either the earliest or latest position in the stream. -//enum ReadPositionConstants { -// READ_POSITION_CONSTANTS_UNSPECIFIED = 0; -// READ_POSITION_CONSTANTS_EARLIEST = 1; -// READ_POSITION_CONSTANTS_LATEST = 2; -//} -// -//// Represents the successful outcome of a read operation. -//message ReadSuccess { -// repeated Record records = 1; -//} -// -//// Represents the detailed error information when a read operation fails. -//message ReadFailure { -// // The error details -// oneof error { -// // Failed because the client lacks sufficient permissions. -// ErrorDetails.AccessDenied access_denied = 1; -// // Failed because the target stream has been deleted. -// ErrorDetails.StreamDeleted stream_deleted = 2; -// // Failed because the expected stream revision did not match the actual revision. -// ErrorDetails.StreamNotFound stream_not_found = 3; -// } -//} -// -//message ReadRequest { -// // The filter to apply when reading records. -// optional ReadFilter filter = 1; -// // The starting position of the log from which to read records. -// optional int64 start_position = 2; -// // Limit how many records can be returned. -// // This will get capped at the default limit, -// // which is up to 1000 records. -// optional int64 limit = 3; -// // The direction in which to read the stream (forwards or backwards). -// ReadDirection direction = 4; -// // Heartbeats can be enabled to monitor end-to-end session health. -// HeartbeatOptions heartbeats = 5; -// // The number of records to read in a single batch. -// int32 batch_size = 6; -//} -// -////message SubscriptionConfirmed { -//// // The subscription ID that was confirmed. -//// string subscription_id = 1; -//// // The position of the last record read by the server. -//// optional int64 position = 2; -//// // When the subscription was confirmed. -//// google.protobuf.Timestamp timestamp = 3; -////} -// -//// Read session response. -//message ReadResponse { -// oneof result { -// // Success represents the successful outcome of an read operation. -// ReadSuccess success = 1; -// // Failure represents the details of a failed read operation. -// ReadFailure failure = 2; -// // Heartbeat represents the health check of the read operation when -// // the server has not found any records matching the filter for the specified -// // period of time or records threshold. -// // A heartbeat will be sent when the initial switch to real-time tailing happens. -// Heartbeat heartbeat = 3; -// } -//} -// -//// A health check will be sent when the server has not found any records -//// matching the filter for the specified period of time or records threshold. A -//// heartbeat will be sent when the initial switch to real-time tailing happens. -//message HeartbeatOptions { -// bool enable = 1; -// optional google.protobuf.Duration period = 2; // 30 seconds -// optional int32 records_threshold = 3; // 500 -//} -// -//enum HeartbeatType { -// HEARTBEAT_TYPE_UNSPECIFIED = 0; -// HEARTBEAT_TYPE_CHECKPOINT = 1; -// HEARTBEAT_TYPE_CAUGHT_UP = 2; -// HEARTBEAT_TYPE_FELL_BEHIND = 3; -//} -// -//message Heartbeat { -// // This indicates whether the subscription is caught up, fell behind, or -// // the filter has not been satisfied after a period of time or records threshold. -// HeartbeatType type = 1; -// // Checkpoint for resuming reads. -// // It will always be populated unless the database is empty. -// int64 position = 2; -// // When the heartbeat was sent. -// google.protobuf.Timestamp timestamp = 3; -//} +// Constants that match the expected state of a stream during an +// append operation. It can be used to specify whether the stream should exist, +// not exist, or can be in any state. +enum ExpectedRevisionConstants { + // The stream should exist and have a single event. + EXPECTED_REVISION_CONSTANTS_SINGLE_EVENT = 0; + + // It is not important whether the stream exists or not. + EXPECTED_REVISION_CONSTANTS_ANY = -2; + + // The stream should not exist. If it does, the append will fail. + EXPECTED_REVISION_CONSTANTS_NO_STREAM = -1; -////=================================================================== -//// Read Operations -////=================================================================== -// -//enum ConsumeFilterScope { -// CONSUME_FILTER_SCOPE_UNSPECIFIED = 0; -// // The filter will be applied to the stream name -// CONSUME_FILTER_SCOPE_STREAM = 1; -// // The filter will be applied to the record schema name -// CONSUME_FILTER_SCOPE_RECORD = 2; -// // The filter will be applied to the properties of record -// CONSUME_FILTER_SCOPE_PROPERTIES = 3; -// // The filter will be applied to the record data -// CONSUME_FILTER_SCOPE_DATA = 4; -//} -// -//// The filter to apply when reading records from the database -//// It applies to a stream or a record -//message ConsumeFilter { -// // The scope of the filter. -// ConsumeFilterScope scope = 1; -// // The expression can be a regular expression, a jsonpath expression, or a literal value. -// // if it starts with "~" it will be considered a regex and if it starts with "$" it will be considered a jsonpath filter, else its a literal. -// string expression = 2; -// // The name of the record property to filter on. -// optional string property_name = 3; -//} -// -//// Record retrieved from the database. -//message Record { -// // The unique identifier of the record in the database. -// string record_id = 1; -// // The position of the record in the database. -// int64 position = 5; -// // The actual data payload of the record, stored as bytes. -// bytes data = 2; -// // Additional information about the record. -// map properties = 3; -// // When the record was created. -// google.protobuf.Timestamp timestamp = 4; -// // The stream to which the record belongs. -// optional string stream = 6; -// // The revision of the stream created when the record was appended. -// optional int64 stream_revision = 7; -//} -// -////// A batch of records. -////message RecordBatch { -//// repeated Record records = 1; -////} -// -//// The direction in which to read records from the database (forwards or backwards). -//enum ReadDirection { -// READ_DIRECTION_FORWARDS = 0; -// READ_DIRECTION_BACKWARDS = 1; -//} -// -//// The position from which to start reading records. -//// This can be either the earliest or latest position in the stream. -//enum ReadPositionConstants { -// READ_POSITION_CONSTANTS_UNSPECIFIED = 0; -// READ_POSITION_CONSTANTS_EARLIEST = 1; -// READ_POSITION_CONSTANTS_LATEST = 2; -//} -// -//message ReadStreamRequest { -// // The filter to apply when reading records. -// optional ConsumeFilter filter = 1; -// // The starting position of the log from which to read records. -// optional int64 start_position = 2; -// // Limit how many records can be returned. -// // This will get capped at the default limit, -// // which is up to 1000 records. -// optional int64 limit = 3; -// // The direction in which to read the stream (forwards or backwards). -// ReadDirection direction = 4; -//} -// -//message ReadStreamSuccess { -// repeated Record records = 1; -//} -// -//// Represents the detailed error information when a read operation fails. -//message ReadStreamFailure { -// // The error details -// oneof error { -// // Failed because the client lacks sufficient permissions. -// ErrorDetails.AccessDenied access_denied = 3; -// // Failed because the target stream has been deleted. -// ErrorDetails.StreamDeleted stream_deleted = 4; -// } -//} -//message ReadStreamResponse { -// // The result of the read operation. -// oneof result { -// // Success represents the successful outcome of an read operation. -// ReadStreamSuccess success = 1; -// // Failure represents the details of a failed read operation. -// ReadStreamFailure failure = 2; -// // Heartbeat represents the health check of the read operation when -// // the server has not found any records matching the filter for the specified -// // period of time or records threshold. -// // A heartbeat will be sent when the initial switch to real-time tailing happens. -// Heartbeat heartbeat = 3; -// } -//} -// -//message ReadSessionRequest { -// // The filter to apply when reading records. -// optional ConsumeFilter filter = 1; -// // The starting position of the log from which to read records. -// optional int64 start_position = 2; -// // Limit how many records can be returned. -// // This will get capped at the default limit, -// // which is up to 1000 records. -// optional int64 limit = 3; -// // The direction in which to read the stream (forwards or backwards). -// ReadDirection direction = 4; -// // Heartbeats can be enabled to monitor end-to-end session health. -// HeartbeatOptions heartbeats = 5; -//} -// -//// Read session response. -//message ReadSessionResponse { -// oneof result { -// // Success represents the successful outcome of an read operation. -// ReadStreamSuccess success = 1; -// // Failure represents the details of a failed read operation. -// ReadStreamFailure failure = 2; -// // Heartbeat represents the health check of the read operation when -// // the server has not found any records matching the filter for the specified -// // period of time or records threshold. -// // A heartbeat will be sent when the initial switch to real-time tailing happens. -// Heartbeat heartbeat = 3; -// } -//} -// -//// A health check will be sent when the server has not found any records -//// matching the filter for the specified period of time or records threshold. A -//// heartbeat will be sent when the initial switch to real-time tailing happens. -//message HeartbeatOptions { -// bool enable = 1; -// //optional google.protobuf.Duration period = 2; -// optional int32 records_threshold = 3; // 1000 -//} -// -//enum HeartbeatType { -// HEARTBEAT_TYPE_UNSPECIFIED = 0; -// HEARTBEAT_TYPE_CHECKPOINT = 1; -// HEARTBEAT_TYPE_CAUGHT_UP = 2; -//} -// -//message Heartbeat { -// // This indicates whether the subscription is caught up, fell behind, or -// // the filter has not been satisfied after a period of time or records threshold. -// HeartbeatType type = 1; -// // Checkpoint for resuming reads. -// // It will always be populated unless the database is empty. -// int64 position = 2; -// // When the heartbeat was sent. -// google.protobuf.Timestamp timestamp = 3; -//} + // The stream should exist + EXPECTED_REVISION_CONSTANTS_EXISTS = -4; +} diff --git a/src/KurrentDB.Client/KurrentDB.Client.csproj b/src/KurrentDB.Client/KurrentDB.Client.csproj index 8821fe674..b16474476 100644 --- a/src/KurrentDB.Client/KurrentDB.Client.csproj +++ b/src/KurrentDB.Client/KurrentDB.Client.csproj @@ -8,14 +8,17 @@ - + - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -29,6 +32,7 @@ Link="Core\proto\kurrentdb\protocol\v1\%(RecursiveDir)/%(FileName)%(Extension)" GrpcServices="Client" Access="internal" + AdditionalImportDirs="$(PkgGoogle_Api_CommonProtos)\content\protos" /> @@ -38,6 +42,7 @@ Link="Core\proto\kurrentdb\protocol\v2\%(RecursiveDir)/%(FileName)%(Extension)" GrpcServices="Client" Access="internal" + AdditionalImportDirs="Core/proto;$(PkgGoogle_Api_CommonProtos)\content\protos" /> diff --git a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Read.cs b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Read.cs index 78bfaf168..94d93464f 100644 --- a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Read.cs +++ b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Read.cs @@ -237,7 +237,7 @@ CancellationToken cancellationToken cancellationToken: cancellationToken ); - _channel = Channel.CreateBounded(ReadBoundedChannelOptions); + _channel = System.Threading.Channels.Channel.CreateBounded(ReadBoundedChannelOptions); _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.Append.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.Append.cs index d7f813baa..8f61f0e95 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.Append.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.Append.cs @@ -202,7 +202,7 @@ Action onException _settings = settings; _cancellationToken = cancellationToken; _onException = onException; - _channel = Channel.CreateBounded(10000); + _channel = System.Threading.Channels.Channel.CreateBounded(10000); _pendingRequests = new ConcurrentDictionary>(); _isUsable = new TaskCompletionSource(); diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs index 1ebb2a4b6..81f094f56 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs @@ -1,16 +1,19 @@ // ReSharper disable SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault +// ReSharper disable SwitchStatementMissingSomeEnumCasesNoDefault // ReSharper disable ConvertToPrimaryConstructor // ReSharper disable PossibleMultipleEnumeration #pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). using System.Diagnostics; +using Grpc.Core; using KurrentDB.Client.Diagnostics; using KurrentDB.Diagnostics; using KurrentDB.Diagnostics.Telemetry; -using static KurrentDB.Protocol.Streams.V2.StreamsService; -using static KurrentDB.Protocol.Streams.V2.MultiStreamAppendResponse; -using Contracts = KurrentDB.Protocol.Streams.V2; +using KurrentDB.Protocol.V2.Streams; +using KurrentDB.Protocol.V2.Streams.Errors; +using static KurrentDB.Diagnostics.Tracing.TracingConstants; +using static KurrentDB.Protocol.V2.Streams.StreamsService; namespace KurrentDB.Client; @@ -21,15 +24,14 @@ public partial class KurrentDBClient { /// An asynchronous enumerable of objects, each containing details of the stream, expected stream state, and events to append. /// An optional cancellation token to observe while waiting for the operation to complete. /// - /// A task that represents the asynchronous operation, with a result of type , indicating the outcome of the operation. + /// A task that represents the asynchronous operation, with a result of type , indicating the outcome of the operation. /// - /// On success, returns containing the successful append results. - /// On failure, returns containing a collection of exceptions that may include: + /// On success, returns containing the successful append results. /// , , , or . /// /// /// Thrown if the server does not support multi-stream append functionality (requires server version 25.1 or higher). - public async ValueTask MultiStreamAppendAsync( + public async ValueTask MultiStreamAppendAsync( IAsyncEnumerable requests, CancellationToken cancellationToken = default ) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); @@ -44,38 +46,41 @@ public async ValueTask MultiStreamAppendAsync( .WithClientSettingsServerTags(Settings) .WithOptionalTag(TelemetryTags.Database.User, Settings.DefaultCredentials?.Username); - return await KurrentDBClientDiagnostics.ActivitySource.TraceMultiStreamAppend(Operation, tags); + return await KurrentDBClientDiagnostics.ActivitySource.TraceClientOperation(Operation, Operations.MultiAppend, tags).ConfigureAwait(false); - async ValueTask Operation() { - using var session = client.MultiStreamAppendSession(KurrentDBCallOptions.CreateStreaming(Settings, cancellationToken: cancellationToken)); + async ValueTask Operation() { + try { + using var session = client.AppendSession(KurrentDBCallOptions.CreateStreaming(Settings, cancellationToken: cancellationToken)); - await foreach (var request in requests.WithCancellation(cancellationToken)) { - var records = await request.Messages - .Map() - .ToArrayAsync(cancellationToken) - .ConfigureAwait(false); + await foreach (var request in requests.WithCancellation(cancellationToken)) { + var records = await request.Messages + .Map() + .ToArrayAsync(cancellationToken) + .ConfigureAwait(false); - var serviceRequest = new Contracts.AppendStreamRequest { - Stream = request.Stream, - ExpectedRevision = request.ExpectedState.ToInt64(), - Records = { records } - }; + var serviceRequest = new AppendRequest { + Stream = request.Stream, + ExpectedRevision = request.ExpectedState.ToInt64(), + Records = { records } + }; - // Cancellation of stream writes is not supported by this gRPC implementation. - // To cancel the operation, we should cancel the entire session. - await session.RequestStream - .WriteAsync(serviceRequest) - .ConfigureAwait(false); - } + // Cancellation of stream writes is not supported by this gRPC implementation. + // To cancel the operation, we should cancel the entire session. + await session.RequestStream + .WriteAsync(serviceRequest) + .ConfigureAwait(false); + } + + await session.RequestStream.CompleteAsync(); - await session.RequestStream.CompleteAsync(); + var response = await session.ResponseAsync; - var response = await session.ResponseAsync; + var responses = response.Output.Select(appendResponse => new AppendResponse(appendResponse.Stream, appendResponse.StreamRevision)); - return response.ResultCase switch { - ResultOneofCase.Success => new MultiAppendSuccess(response.Success.Map()), - ResultOneofCase.Failure => new MultiAppendFailure(response.Failure.Map()) - }; + return new MultiStreamAppendResponse(response.Position, responses); + } catch (RpcException ex) { + throw ex.MapRpcException(); + } } } } diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.Read.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.Read.cs index 1d1d361c6..aa01d994e 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.Read.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.Read.cs @@ -150,7 +150,7 @@ CancellationToken cancellationToken cancellationToken ); - _channel = Channel.CreateBounded(ReadBoundedChannelOptions); + _channel = System.Threading.Channels.Channel.CreateBounded(ReadBoundedChannelOptions); _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var linkedCancellationToken = _cts.Token; @@ -349,7 +349,7 @@ CancellationToken cancellationToken cancellationToken ); - _channel = Channel.CreateBounded(ReadBoundedChannelOptions); + _channel = System.Threading.Channels.Channel.CreateBounded(ReadBoundedChannelOptions); StreamName = request.Options.Stream.StreamIdentifier!; diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.Subscriptions.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.Subscriptions.cs index 72ce47ca5..9647d39de 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.Subscriptions.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.Subscriptions.cs @@ -187,7 +187,7 @@ CancellationToken cancellationToken cancellationToken: cancellationToken ); - _channel = Channel.CreateBounded(ReadBoundedChannelOptions); + _channel = System.Threading.Channels.Channel.CreateBounded(ReadBoundedChannelOptions); _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); diff --git a/src/KurrentDB.Client/Streams/MultiStreamAppend.Extensions.cs b/src/KurrentDB.Client/Streams/MultiStreamAppend.Extensions.cs index c1eff6a25..e9c850abc 100644 --- a/src/KurrentDB.Client/Streams/MultiStreamAppend.Extensions.cs +++ b/src/KurrentDB.Client/Streams/MultiStreamAppend.Extensions.cs @@ -1,12 +1,12 @@ namespace KurrentDB.Client; public static class MultiStreamAppendExtensions { - public static ValueTask MultiStreamAppendAsync( + public static ValueTask MultiStreamAppendAsync( this KurrentDBClient client, IEnumerable requests, CancellationToken cancellationToken = default ) => client.MultiStreamAppendAsync(requests.ToAsyncEnumerable(), cancellationToken); - public static ValueTask MultiStreamAppendAsync( + public static ValueTask MultiStreamAppendAsync( this KurrentDBClient client, AppendStreamRequest request, CancellationToken cancellationToken = default ) => client.MultiStreamAppendAsync([request], cancellationToken); @@ -18,19 +18,19 @@ public static ValueTask MultiStreamAppendAsync( /// The expected state of the stream to ensure consistency during the append operation. /// A collection of messages to be appended to the stream. /// A token to observe while waiting for the operation to complete, allowing for cancellation if needed. - public static ValueTask MultiStreamAppendAsync( + public static ValueTask MultiStreamAppendAsync( this KurrentDBClient client, string stream, StreamState expectedState, IEnumerable messages, CancellationToken cancellationToken ) => client.MultiStreamAppendAsync(new AppendStreamRequest(stream, expectedState, messages), cancellationToken); - public static ValueTask MultiStreamAppendAsync( + public static ValueTask MultiStreamAppendAsync( this KurrentDBClient client, string stream, StreamState expectedState, EventData message, CancellationToken cancellationToken ) => client.MultiStreamAppendAsync(new AppendStreamRequest(stream, expectedState, [message]), cancellationToken); - public static ValueTask MultiStreamAppendAsync( + public static ValueTask MultiStreamAppendAsync( this KurrentDBClient client, string stream, IEnumerable messages, CancellationToken cancellationToken ) => client.MultiStreamAppendAsync(new AppendStreamRequest(stream, StreamState.Any, messages), cancellationToken); diff --git a/src/KurrentDB.Client/Streams/Streams/BatchAppendResp.cs b/src/KurrentDB.Client/Streams/Streams/BatchAppendResp.cs index 11120e62e..44ec7984f 100644 --- a/src/KurrentDB.Client/Streams/Streams/BatchAppendResp.cs +++ b/src/KurrentDB.Client/Streams/Streams/BatchAppendResp.cs @@ -19,19 +19,19 @@ partial class BatchAppendResp { Success.Position.PreparePosition), _ => Position.End }), - ResultOneofCase.Error => Error.Details switch { - { } when Error.Details.Is(WrongExpectedVersion.Descriptor) => - FromWrongExpectedVersion(StreamIdentifier, Error.Details.Unpack()), - { } when Error.Details.Is(StreamDeleted.Descriptor) => + ResultOneofCase.Error => Error.Details.FirstOrDefault() switch { + { } detail when detail.Is(WrongExpectedVersion.Descriptor) => + FromWrongExpectedVersion(StreamIdentifier, detail.Unpack()), + { } detail when detail.Is(StreamDeleted.Descriptor) => throw new StreamDeletedException(StreamIdentifier!), - { } when Error.Details.Is(AccessDenied.Descriptor) => throw new AccessDeniedException(), - { } when Error.Details.Is(Timeout.Descriptor) => throw new RpcException( + { } detail when detail.Is(AccessDenied.Descriptor) => throw new AccessDeniedException(), + { } detail when detail.Is(Timeout.Descriptor) => throw new RpcException( new Status(StatusCode.DeadlineExceeded, Error.Message)), - { } when Error.Details.Is(Unknown.Descriptor) => throw new InvalidOperationException(Error.Message), - { } when Error.Details.Is(MaximumAppendSizeExceeded.Descriptor) => + { } detail when detail.Is(Unknown.Descriptor) => throw new InvalidOperationException(Error.Message), + { } detail when detail.Is(MaximumAppendSizeExceeded.Descriptor) => throw new MaximumAppendSizeExceededException( - Error.Details.Unpack().MaxAppendSize), - { } when Error.Details.Is(BadRequest.Descriptor) => throw new InvalidOperationException(Error.Details + detail.Unpack().MaxAppendSize), + { } detail when detail.Is(BadRequest.Descriptor) => throw new InvalidOperationException(detail .Unpack().Message), _ => throw new InvalidOperationException($"Could not recognize {Error.Message}") }, diff --git a/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs b/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs index 84cc4638a..cca9c3b26 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs @@ -1,12 +1,15 @@ // ReSharper disable InconsistentNaming using System.Buffers; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; using JetBrains.Annotations; using System.Text; using KurrentDB.Client.Core.Internal.Exceptions; using static KurrentDB.Client.Constants; +using Enum = System.Enum; +using Type = System.Type; namespace KurrentDB.Client; @@ -65,7 +68,7 @@ public class MetadataJsonConverter : JsonConverter> JsonTokenType.True => true, JsonTokenType.False => false, JsonTokenType.String => ParseString(reader, propertyName), - JsonTokenType.Number => ParseNumber(reader), + JsonTokenType.Number => reader.GetDouble(), _ => throw new JsonException($"Unsupported metadata value type ({reader.TokenType}) for property '{propertyName}'") }; @@ -74,31 +77,14 @@ public class MetadataJsonConverter : JsonConverter> return metadata; - static object ParseNumber(Utf8JsonReader reader) { - if (reader.TryGetInt32(out var intValue)) - return intValue; - - if (reader.TryGetInt64(out var longValue)) - return longValue; - - if (reader.TryGetDouble(out var doubleValue)) - return doubleValue; - -#if NET48 - return Encoding.UTF8.GetString(reader.ValueSequence.ToArray()); -#else - return Encoding.UTF8.GetString(reader.ValueSpan); -#endif - } - static object? ParseString(Utf8JsonReader reader, string propertyName) { - if (propertyName.Equals(Metadata.SchemaName, StringComparison.OrdinalIgnoreCase)) { - var value = reader.GetString(); + var value = reader.GetString(); + + if (propertyName.Equals(Metadata.SchemaName, StringComparison.OrdinalIgnoreCase)) return string.IsNullOrWhiteSpace(value) ? "" : value; - } - if (propertyName.Equals(Metadata.SchemaDataFormat, StringComparison.OrdinalIgnoreCase)) - return Enum.TryParse(reader.GetString(), ignoreCase: true, out var format) + if (propertyName.Equals(Metadata.SchemaFormat, StringComparison.OrdinalIgnoreCase)) + return Enum.TryParse(value, ignoreCase: true, out var format) ? format : SchemaDataFormat.Unspecified; @@ -107,7 +93,10 @@ static object ParseNumber(Utf8JsonReader reader) { if (reader.TryGetTimeSpan(out var timeSpan)) return timeSpan; if (reader.TryGetBytesFromBase64(out var bytes)) return new ReadOnlyMemory(bytes); - return reader.GetString(); + if (DateTime.TryParse(value, null, DateTimeStyles.RoundtripKind, out var isoDateTime)) + return isoDateTime; + + return value; } } diff --git a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs index 407299b0d..32e3b20b9 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs @@ -5,8 +5,12 @@ using System.Diagnostics; using Google.Protobuf; using Google.Protobuf.Collections; +using Grpc.Core; using KurrentDB.Client.Diagnostics; -using KurrentDB.Protocol.Streams.V2; +using KurrentDB.Protocol.V2.Streams; +using KurrentDB.Protocol.V2.Streams.Errors; +using static KurrentDB.Client.Constants.Metadata; +using SchemaFormat = KurrentDB.Protocol.V2.Streams.SchemaFormat; namespace KurrentDB.Client; @@ -24,46 +28,75 @@ public static ValueTask Map(this EventData source) { if (!source.Metadata.IsEmpty) metadata = MetadataDecoder.Decode(source.Metadata); - metadata[Constants.Metadata.SchemaName] = source.Type; - metadata[Constants.Metadata.SchemaDataFormat] = source.ContentType is Constants.Metadata.ContentTypes.ApplicationJson - ? SchemaDataFormat.Json - : SchemaDataFormat.Bytes; - metadata.InjectTracingContext(Activity.Current); var record = new AppendRecord { - RecordId = source.EventId.ToString(), - Data = ByteString.CopyFrom(source.Data.Span), - Properties = { metadata.MapToDynamicMapField() } + RecordId = source.EventId.ToString(), + Data = ByteString.CopyFrom(source.Data.Span), + Schema = new SchemaInfo { + Format = source.ContentType is ContentTypes.ApplicationJson + ? SchemaFormat.Json + : SchemaFormat.Bytes, + Name = source.Type + }, + Properties = { metadata.MapToMapValue() } }; return new ValueTask(record); } - public static Exception Map(this AppendStreamFailure source) { - return source.ErrorCase switch { - AppendStreamFailure.ErrorOneofCase.StreamRevisionConflict => new WrongExpectedVersionException( - source.Stream, - StreamState.StreamRevision((ulong)source.StreamRevisionConflict.StreamRevision) - ), - AppendStreamFailure.ErrorOneofCase.AccessDenied => new AccessDeniedException(), - AppendStreamFailure.ErrorOneofCase.StreamDeleted => new StreamDeletedException(source.Stream), - AppendStreamFailure.ErrorOneofCase.TransactionMaxSizeExceeded => new TransactionMaxSizeExceededException(source.TransactionMaxSizeExceeded.MaxSize), + public static AppendResponse Map(this KurrentDB.Protocol.V2.Streams.AppendResponse source) => new(source.Stream, source.StreamRevision); + + public static IEnumerable Map(this RepeatedField source) => + source.Select(response => response.Map()); + + public static Exception MapRpcException(this RpcException ex) { + var status = ex.GetRpcStatus(); + + return ex.StatusCode switch { + StatusCode.Aborted => HandleAborted(ex, status), + StatusCode.FailedPrecondition => HandleFailedPrecondition(ex, status), + StatusCode.NotFound => HandleNotFound(ex, status), + StatusCode.InvalidArgument => HandleInvalidArgument(ex, status), + _ => ex }; } - public static AppendStreamSuccess Map(this Protocol.Streams.V2.AppendStreamSuccess source) => - new(source.Stream, source.Position); + static Exception HandleFailedPrecondition(RpcException ex, Google.Rpc.Status? status) { + var revisionConflict = status?.GetDetail(); + if (revisionConflict != null) { + return new WrongExpectedVersionException( + revisionConflict.Stream, + StreamState.StreamRevision((ulong)revisionConflict.ExpectedRevision), + StreamState.StreamRevision((ulong)revisionConflict.ActualRevision), + ex + ); + } + + var tombstoned = status?.GetDetail(); + if (tombstoned != null) return new StreamDeletedException(tombstoned.Stream, ex); + + return ex; + } + + static Exception HandleNotFound(RpcException ex, Google.Rpc.Status? status) { + var notFound = status?.GetDetail(); + if (notFound != null) return new StreamNotFoundException(notFound.Stream, ex); - public static AppendStreamFailures Map(this RepeatedField source) => - new(source.Select(failure => failure.Map())); + return ex; + } - public static AppendStreamSuccesses Map(this RepeatedField source) => - new(source.Select(success => success.Map())); + static Exception HandleInvalidArgument(RpcException ex, Google.Rpc.Status? status) { + var recordSizeExceeded = status?.GetDetail(); + if (recordSizeExceeded != null) return new MaximumAppendSizeExceededException(recordSizeExceeded.MaxSize, ex); - public static AppendStreamFailures Map(this MultiStreamAppendResponse.Types.Failure source) => - new(source.Output.Map()); + return ex; + } - public static AppendStreamSuccesses Map(this MultiStreamAppendResponse.Types.Success source) => - new(source.Output.Map()); + static Exception HandleAborted(RpcException ex, Google.Rpc.Status? status) { + var transactionSizeExceeded = status?.GetDetail(); + if (transactionSizeExceeded != null) return new TransactionMaxSizeExceededException(transactionSizeExceeded.MaxSize, ex); + + return ex; + } } diff --git a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientModel.cs b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientModel.cs index 3e59a01ad..4d2d0dd87 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientModel.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientModel.cs @@ -20,55 +20,11 @@ namespace KurrentDB.Client; [PublicAPI] public record AppendStreamRequest(string Stream, StreamState ExpectedState, IEnumerable Messages); -/// -/// Represents the successful outcome of an append operation to a specific stream in the system. -/// -/// -/// The name of the stream where the events have been successfully appended. -/// -/// -/// The position in the stream after the append operation, indicating where the event(s) were written. -/// [PublicAPI] -public record AppendStreamSuccess(string Stream, long Position); - -/// -/// Represents the result of a multi-stream append operation in KurrentDB. -/// -/// -/// -[PublicAPI] -public abstract class MultiAppendWriteResult { - public abstract bool IsSuccess { get; } - public bool IsFailure => !IsSuccess; -} - -[PublicAPI] -public sealed class MultiAppendSuccess : MultiAppendWriteResult { - public override bool IsSuccess => true; - public AppendStreamSuccesses Successes { get; } - - internal MultiAppendSuccess(AppendStreamSuccesses successes) => - Successes = successes; -} - -[PublicAPI] -public sealed class MultiAppendFailure : MultiAppendWriteResult { - public override bool IsSuccess => false; - public AppendStreamFailures Failures { get; } - - internal MultiAppendFailure(AppendStreamFailures failures) => - Failures = failures; -} - -[PublicAPI] -public class AppendStreamSuccesses : List { - public AppendStreamSuccesses() { } - public AppendStreamSuccesses(IEnumerable input) : base(input) { } -} +public record AppendResponse(string Stream, long StreamRevision); [PublicAPI] -public class AppendStreamFailures : List { - public AppendStreamFailures() { } - public AppendStreamFailures(IEnumerable input) : base(input) { } +public readonly struct MultiStreamAppendResponse(long position, IEnumerable? responses = null) { + public readonly long Position = position; + public readonly IEnumerable? Responses = responses; } diff --git a/test/KurrentDB.Client.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs b/test/KurrentDB.Client.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs index 4e3b23f15..8d4e07f82 100644 --- a/test/KurrentDB.Client.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs +++ b/test/KurrentDB.Client.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs @@ -9,8 +9,7 @@ namespace KurrentDB.Client.Tests.Diagnostics; [Trait("Category", "Target:Diagnostics")] -public class StreamsTracingInstrumentationTests(ITestOutputHelper output, DiagnosticsFixture fixture) - : KurrentDBPermanentTests(output, fixture) { +public class StreamsTracingInstrumentationTests(ITestOutputHelper output, DiagnosticsFixture fixture) : KurrentDBPermanentTests(output, fixture) { [Fact] public async Task append_to_stream() { var traceId = Fixture.CreateTraceId(); @@ -46,7 +45,7 @@ public async Task multi_stream_append() { AppendStreamRequest[] requests = [new(stream1, StreamState.NoStream, seedEvents.Take(5)), new(stream2, StreamState.NoStream, seedEvents.Skip(5))]; // Act - var multiStreamAppendResult = await Fixture.Streams.MultiStreamAppendAsync(requests.ToAsyncEnumerable()); + var appendResult = await Fixture.Streams.MultiStreamAppendAsync(requests.ToAsyncEnumerable()); await using var subscription = Fixture.Streams.SubscribeToAll( FromAll.Start, @@ -58,7 +57,7 @@ public async Task multi_stream_append() { await Subscribe().WithTimeout(); // Assert - multiStreamAppendResult.IsSuccess.ShouldBeTrue(); + appendResult.Position.ShouldBePositive(); var appendActivities = Fixture.GetActivities(TracingConstants.Operations.MultiAppend, traceId); var subscribeActivities = Fixture.GetActivities(TracingConstants.Operations.Subscribe, traceId); @@ -104,7 +103,7 @@ async Task Subscribe() { } [MinimumVersion.Fact(25, 1)] - public async Task multi_stream_append_with_failures() { + public async Task multi_stream_append_with_exceptions() { var traceId = Fixture.CreateTraceId(); // Arrange @@ -117,11 +116,10 @@ public async Task multi_stream_append_with_failures() { ]; // Act - var multiStreamAppendResult = await Fixture.Streams.MultiStreamAppendAsync(requests); + var appendTask = async () => await Fixture.Streams.MultiStreamAppendAsync(requests); + var rex = await appendTask.ShouldThrowAsync(); // Assert - multiStreamAppendResult.IsFailure.ShouldBeTrue(); - var appendActivities = Fixture.GetActivities(TracingConstants.Operations.MultiAppend, traceId); appendActivities.ShouldNotBeEmpty(); @@ -130,13 +128,14 @@ public async Task multi_stream_append_with_failures() { var activity = appendActivities.FirstOrDefault().ShouldNotBeNull(); activity.Status.ShouldBe(ActivityStatusCode.Error); - activity.Events.Count().ShouldBe(2); + activity.Events.ShouldHaveSingleItem(); - activity.Events.ShouldAllBe(activityEvent => - activityEvent.Tags.Any(tag => tag.Key == TelemetryTags.Exception.Message) && - activityEvent.Tags.Any(tag => tag.Key == TelemetryTags.Exception.Stacktrace) && - activityEvent.Tags.Any(tag => tag.Key == TelemetryTags.Exception.Type && (string?)tag.Value == typeof(WrongExpectedVersionException).FullName) - ); + var activityEvent = activity.Events.First(); + + activityEvent.Name.ShouldBe(TelemetryTags.Exception.EventName); + activityEvent.Tags.Any(tag => tag.Key == TelemetryTags.Exception.Message).ShouldBeTrue(); + activityEvent.Tags.Any(tag => tag.Key == TelemetryTags.Exception.Stacktrace).ShouldBeTrue(); + activityEvent.Tags.Any(tag => tag.Key == TelemetryTags.Exception.Type && (string?)tag.Value == rex.GetType().FullName).ShouldBeTrue(); } [Fact] diff --git a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs index 0937b09b3..f4b1eb4d9 100644 --- a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs +++ b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs @@ -1,13 +1,11 @@ using System.Text.Json; using Humanizer; -using static KurrentDB.Client.Constants; namespace KurrentDB.Client.Tests.Streams; [Trait("Category", "Target:Streams")] [Trait("Category", "Operation:MultiStreamAppend")] -public class MultiStreamAppendTests(ITestOutputHelper output, KurrentDBPermanentFixture fixture) - : KurrentDBPermanentTests(output, fixture) { +public class MultiStreamAppendTests(ITestOutputHelper output, KurrentDBPermanentFixture fixture) : KurrentDBPermanentTests(output, fixture) { [MinimumVersion.Fact(25, 1)] public async Task append_events_with_invalid_metadata_format_throws_exceptions() { // Arrange @@ -15,7 +13,9 @@ public async Task append_events_with_invalid_metadata_format_throws_exceptions() var invalidMetadata = "invalid"u8.ToArray(); - AppendStreamRequest[] requests = [new(stream, StreamState.NoStream, Fixture.CreateTestEvents(3, metadata: invalidMetadata)),]; + AppendStreamRequest[] requests = [ + new(stream, StreamState.NoStream, Fixture.CreateTestEvents(3, metadata: invalidMetadata)) + ]; // Act & Assert var exception = await Fixture.Streams @@ -54,9 +54,14 @@ public async Task append_events_to_multiple_streams() { var result = await Fixture.Streams.MultiStreamAppendAsync(requests); // Assert - result.IsSuccess.ShouldBeTrue(); - var success = result.ShouldBeOfType(); - success.Successes.Count.ShouldBe(2); + result.Position.ShouldBePositive(); + result.Responses.ShouldNotBeEmpty(); + + result.Responses.First().Stream.ShouldBe(stream1); + result.Responses.First().StreamRevision.ShouldBePositive(); + + result.Responses.Last().Stream.ShouldBe(stream2); + result.Responses.Last().StreamRevision.ShouldBePositive(); var stream1Events = await Fixture.Streams .ReadStreamAsync(Direction.Forwards, stream1, StreamPosition.Start, 10) @@ -72,12 +77,14 @@ public async Task append_events_to_multiple_streams() { stream2Events.Length.ShouldBe(2); metadata.ShouldNotBeNull(); - metadata[Metadata.SchemaName].ShouldBe("test-event-type"); - metadata[Metadata.SchemaDataFormat].ShouldBe(SchemaDataFormat.Json); + metadata[Constants.Metadata.SchemaName].ShouldBe("test-event-type"); + metadata[Constants.Metadata.SchemaFormat].ShouldBe(SchemaDataFormat.Json); metadata["StringValue"].ShouldBe(expectedMetadata.StringValue); metadata["BooleanValue"].ShouldBe(expectedMetadata.BooleanValue); + metadata["IntegerValue"].ShouldBe(expectedMetadata.IntegerValue); metadata["DoubleValue"].ShouldBe(expectedMetadata.DoubleValue); + metadata["DateTimeValue"].ShouldBe(expectedMetadata.DateTimeValue); metadata["TimeSpanValue"].ShouldBe(expectedMetadata.TimeSpanValue); metadata["NullTimeSpanValue"].ShouldBeNull(); @@ -86,7 +93,7 @@ public async Task append_events_to_multiple_streams() { metadata["BooleanValue"]?.GetType().ShouldBe(typeof(bool)); metadata["StringValue"]?.GetType().ShouldBe(typeof(string)); - metadata["IntegerValue"]?.GetType().ShouldBe(typeof(int)); + metadata["IntegerValue"]?.GetType().ShouldBe(typeof(double)); metadata["DoubleValue"]?.GetType().ShouldBe(typeof(double)); metadata["DateTimeValue"]?.GetType().ShouldBe(typeof(DateTime)); metadata["TimeSpanValue"]?.GetType().ShouldBe(typeof(TimeSpan)); @@ -96,29 +103,44 @@ public async Task append_events_to_multiple_streams() { } [MinimumVersion.Fact(25, 1)] - public async Task appending_events_with_failures() { + public async Task appending_events_with_stream_revision_conflicts() { // Arrange var stream1 = Fixture.GetStreamName(); var stream2 = Fixture.GetStreamName(); - var stream3 = Fixture.GetStreamName(); AppendStreamRequest[] requests = [ - new(stream1, StreamState.StreamExists, Fixture.CreateTestEvents(3).ToArray()), // does not exist - new(stream2, StreamState.NoStream, Fixture.CreateTestEvents(2).ToArray()), - new(stream3, StreamState.StreamExists, Fixture.CreateTestEvents(3).ToArray()), // does not exist + new(stream1, StreamState.StreamExists, Fixture.CreateTestEvents(3).ToArray()), + new(stream2, StreamState.StreamExists, Fixture.CreateTestEvents(3).ToArray()), ]; // Act - var result = await Fixture.Streams.MultiStreamAppendAsync(requests); + var appendTask = async () => await Fixture.Streams.MultiStreamAppendAsync(requests); // Assert - result.IsFailure.ShouldBeTrue(); - var failure = result.ShouldBeOfType(); - failure.Failures.Count.ShouldBe(2); + var rex = await appendTask.ShouldThrowAsync(); + rex.ExpectedStreamState.ShouldBe(StreamState.StreamExists); + rex.ActualStreamState.ShouldBe(StreamState.NoStream); + } + + [MinimumVersion.Fact(25, 1)] + public async Task appending_events_throws_deleted_exception_when_tombstoned() { + // Arrange + var stream = Fixture.GetStreamName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, Fixture.CreateTestEvents()); + + await Fixture.Streams.TombstoneAsync(stream, StreamState.StreamExists); + + AppendStreamRequest[] requests = [ + new(stream, StreamState.NoStream, Fixture.CreateTestEvents(3).ToArray()) + ]; - failure.Failures - .Select(f => f.ShouldBeOfType()) - .Count().ShouldBe(2); + // Act + var appendTask = async () => await Fixture.Streams.MultiStreamAppendAsync(requests); + + // Assert + var rex = await appendTask.ShouldThrowAsync(); + rex.Stream.ShouldBe(stream); } } From 336d8d18c5abdfd8058a873a7418d83d45a926eb Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 1 Oct 2025 14:59:22 +0400 Subject: [PATCH 23/29] Update docs --- docs/api/appending-events.md | 76 +++---------------- .../TransactionMaxSizeExceededException.cs | 19 +++-- .../AppendRecordSizeExceededException.cs | 33 ++++++++ .../MaximumAppendSizeExceededException.cs | 41 ++++++---- .../Streams/Model/StreamsClientMapper.cs | 26 +++---- .../Streams/Model/StreamsClientModel.cs | 18 ++++- 6 files changed, 110 insertions(+), 103 deletions(-) create mode 100644 src/KurrentDB.Client/Streams/AppendRecordSizeExceededException.cs diff --git a/docs/api/appending-events.md b/docs/api/appending-events.md index 00cc27654..e713d36b9 100644 --- a/docs/api/appending-events.md +++ b/docs/api/appending-events.md @@ -146,38 +146,6 @@ This feature is only available in KurrentDB 25.1 and later. You can append events to multiple streams in a single atomic operation. Either all streams are updated, or the entire operation fails. -The `MultiStreamAppendAsync` method accepts a collection of `AppendStreamRequest` objects and returns a `MultiAppendWriteResult`. Each `AppendStreamRequest` contains: - -- **Stream** - The name of the stream -- **ExpectedState** - The expected state of the stream for optimistic concurrency control -- **Messages** - A collection of `EventData` objects to append - -The operation returns either: -- `MultiAppendSuccess` - Successful append results for all streams -- `MultiAppendFailure` - Specific exceptions for any failed operations - -::: warning -Event metadata in `EventData` must currently be valid JSON that can be deserialized into a -`Dictionary`. This means any metadata you attach to an event should be structured as a JSON object, not as a primitive value or array. - -For example, to encode metadata: -```cs -var metadata = JsonSerializer.SerializeToUtf8Bytes(new { - Timestamp = DateTime.UtcNow, - Source = "OrderProcessingSystem", - Version = 1.0 -}); -``` -To decode metadata back into a dictionary: -```cs -var dictionary = MetadataDecoder.Decode(metadataBytes); -``` - -This requirement ensures compatibility with KurrentDB's current metadata handling. In a future major release, this restriction will be lifted, allowing more flexible metadata formats. -::: - -Here's a basic example of appending events to multiple streams: - ```cs using System.Text.Json; @@ -198,41 +166,19 @@ AppendStreamRequest[] requests = [ ) ]; -var result = await client.MultiStreamAppendAsync(requests); - -if (result is MultiAppendSuccess { Successes: var successes }) - foreach (var item in successes) - Console.WriteLine($"Stream '{item.Stream}' updated at position {item.Position}"); +await client.MultiStreamAppendAsync(requests); ``` -If the operation doesn't succeed, you should check if the result is a `MultiAppendFailure` and handle each failure case appropriately. This allows you to respond to specific errors, such as version conflicts, access issues, deleted streams, or transaction size limits. For example: +The result returns the position of the last appended record in the transaction and a collection of responses for each stream appended in the transaction. +::: warning +If you are storing metadata, it must currently be a valid JSON that can be deserialized into a +`Dictionary`. This means any metadata you attach to an event should be structured as a JSON object, not as a primitive value or array. + +When reading those events you can use the metadata decoder utility class to decode your metadata: ```cs -var result = await client.MultiStreamAppendAsync(requests); - -if (result is MultiAppendFailure { Failures: var failures }) { - foreach (var error in failures) { - switch (error) { - case WrongExpectedVersionException ex: - Console.WriteLine($"Version conflict in stream: {ex.Message}"); - break; - - case AccessDeniedException: - Console.WriteLine("Access denied to one or more streams"); - break; - - case StreamDeletedException ex: - Console.WriteLine($"Stream was deleted: {ex.Message}"); - break; - - case TransactionMaxSizeExceededException ex: - Console.WriteLine($"Transaction too large: {ex.Message}"); - break; - - default: - Console.WriteLine($"Unexpected error: {error.Message}"); - break; - } - } -} +var dictionary = MetadataDecoder.Decode(metadataBytes); ``` + +This requirement ensures compatibility with KurrentDB's current metadata handling, and the restriction will be lifted in the next major release. +::: diff --git a/src/KurrentDB.Client/Core/Exceptions/TransactionMaxSizeExceededException.cs b/src/KurrentDB.Client/Core/Exceptions/TransactionMaxSizeExceededException.cs index 3f46a9689..f54fc9826 100644 --- a/src/KurrentDB.Client/Core/Exceptions/TransactionMaxSizeExceededException.cs +++ b/src/KurrentDB.Client/Core/Exceptions/TransactionMaxSizeExceededException.cs @@ -1,17 +1,20 @@ namespace KurrentDB.Client; -#pragma warning disable CS8509 /// /// Exception thrown when a transaction exceeds the allowed maximum size limit. /// -public class TransactionMaxSizeExceededException : Exception { +public class TransactionMaxSizeExceededException(int size, int maxSize, Exception? innerException = null) + : Exception( + $"The total size of the append transaction ({size}) exceeds the maximum allowed size of {maxSize} bytes by {size - maxSize}", + innerException + ) { /// - /// The maximum size, in bytes, allowed for a transaction before it is considered invalid. + /// The size of the huge transaction in bytes. /// - public readonly long MaxSize; + public int Size { get; } = size; - public TransactionMaxSizeExceededException(long maxSize, Exception? exception = null) - : base($"Transaction size exceeded the maximum allowed size of {maxSize} bytes.", exception) { - MaxSize = maxSize; - } + /// + /// The maximum allowed size of the append transaction in bytes. + /// + public int MaxSize { get; } = maxSize; } diff --git a/src/KurrentDB.Client/Streams/AppendRecordSizeExceededException.cs b/src/KurrentDB.Client/Streams/AppendRecordSizeExceededException.cs new file mode 100644 index 000000000..98a231e05 --- /dev/null +++ b/src/KurrentDB.Client/Streams/AppendRecordSizeExceededException.cs @@ -0,0 +1,33 @@ +using System; + +namespace KurrentDB.Client { + /// + /// Exception thrown when an append exceeds the maximum size set by the server. + /// + public class MaximumAppendSizeExceededException : Exception { + /// + /// The configured maximum append size. + /// + public uint MaxAppendSize { get; } + + /// + /// Constructs a new . + /// + /// + /// + public MaximumAppendSizeExceededException(uint maxAppendSize, Exception? innerException = null) : + base($"Maximum Append Size of {maxAppendSize} Exceeded.", innerException) { + MaxAppendSize = maxAppendSize; + } + + /// + /// Constructs a new . + /// + /// + /// + public MaximumAppendSizeExceededException(int maxAppendSize, Exception? innerException = null) : this( + (uint)maxAppendSize, innerException) { + + } + } +} diff --git a/src/KurrentDB.Client/Streams/MaximumAppendSizeExceededException.cs b/src/KurrentDB.Client/Streams/MaximumAppendSizeExceededException.cs index 98a231e05..07e59ad04 100644 --- a/src/KurrentDB.Client/Streams/MaximumAppendSizeExceededException.cs +++ b/src/KurrentDB.Client/Streams/MaximumAppendSizeExceededException.cs @@ -4,30 +4,41 @@ namespace KurrentDB.Client { /// /// Exception thrown when an append exceeds the maximum size set by the server. /// - public class MaximumAppendSizeExceededException : Exception { + public class AppendRecordSizeExceededException : Exception { /// - /// The configured maximum append size. + /// The name of the stream where the append was attempted. /// - public uint MaxAppendSize { get; } + public string Stream { get; } /// - /// Constructs a new . + /// The identifier of the offending and oversized record. /// - /// - /// - public MaximumAppendSizeExceededException(uint maxAppendSize, Exception? innerException = null) : - base($"Maximum Append Size of {maxAppendSize} Exceeded.", innerException) { - MaxAppendSize = maxAppendSize; - } + public string RecordId { get; } + + /// + /// The size of the huge record in bytes. + /// + public long Size { get; } /// - /// Constructs a new . + /// The maximum allowed size of a single record that can be appended in bytes. /// - /// - /// - public MaximumAppendSizeExceededException(int maxAppendSize, Exception? innerException = null) : this( - (uint)maxAppendSize, innerException) { + public long MaxSize { get; } + /// + /// Constructs a new . + /// + /// The name of the stream where the append was attempted. + /// The identifier of the offending and oversized record. + /// The size of the huge record in bytes. + /// The maximum allowed size of a single record that can be appended in bytes. + /// The inner exception, if any. + public AppendRecordSizeExceededException(string stream, string recordId, long size, long maxSize, Exception? innerException = null) + : base($"The size of the record {recordId} ({size}) exceeds by maximum allowed size of {maxSize} bytes by {size - maxSize}", innerException) { + Stream = stream; + RecordId = recordId; + Size = size; + MaxSize = maxSize; } } } diff --git a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs index 32e3b20b9..ea28a3af0 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using Google.Protobuf; -using Google.Protobuf.Collections; using Grpc.Core; using KurrentDB.Client.Diagnostics; using KurrentDB.Protocol.V2.Streams; @@ -47,16 +46,12 @@ public static ValueTask Map(this EventData source) { public static AppendResponse Map(this KurrentDB.Protocol.V2.Streams.AppendResponse source) => new(source.Stream, source.StreamRevision); - public static IEnumerable Map(this RepeatedField source) => - source.Select(response => response.Map()); - public static Exception MapRpcException(this RpcException ex) { var status = ex.GetRpcStatus(); return ex.StatusCode switch { StatusCode.Aborted => HandleAborted(ex, status), StatusCode.FailedPrecondition => HandleFailedPrecondition(ex, status), - StatusCode.NotFound => HandleNotFound(ex, status), StatusCode.InvalidArgument => HandleInvalidArgument(ex, status), _ => ex }; @@ -79,23 +74,26 @@ static Exception HandleFailedPrecondition(RpcException ex, Google.Rpc.Status? st return ex; } - static Exception HandleNotFound(RpcException ex, Google.Rpc.Status? status) { - var notFound = status?.GetDetail(); - if (notFound != null) return new StreamNotFoundException(notFound.Stream, ex); - - return ex; - } - static Exception HandleInvalidArgument(RpcException ex, Google.Rpc.Status? status) { var recordSizeExceeded = status?.GetDetail(); - if (recordSizeExceeded != null) return new MaximumAppendSizeExceededException(recordSizeExceeded.MaxSize, ex); + if (recordSizeExceeded != null) return new AppendRecordSizeExceededException( + recordSizeExceeded.Stream, + recordSizeExceeded.RecordId, + recordSizeExceeded.Size, + recordSizeExceeded.MaxSize, + ex + ); return ex; } static Exception HandleAborted(RpcException ex, Google.Rpc.Status? status) { var transactionSizeExceeded = status?.GetDetail(); - if (transactionSizeExceeded != null) return new TransactionMaxSizeExceededException(transactionSizeExceeded.MaxSize, ex); + if (transactionSizeExceeded != null) return new TransactionMaxSizeExceededException( + transactionSizeExceeded.Size, + transactionSizeExceeded.MaxSize, + ex + ); return ex; } diff --git a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientModel.cs b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientModel.cs index 4d2d0dd87..7ada568e5 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientModel.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientModel.cs @@ -20,11 +20,27 @@ namespace KurrentDB.Client; [PublicAPI] public record AppendStreamRequest(string Stream, StreamState ExpectedState, IEnumerable Messages); +/// +/// Represents the outcome of an append operation. +/// +/// +/// The name of the stream to which records were appended. +/// +/// +/// The expected revision of the stream after the append operation. +/// [PublicAPI] public record AppendResponse(string Stream, long StreamRevision); [PublicAPI] public readonly struct MultiStreamAppendResponse(long position, IEnumerable? responses = null) { - public readonly long Position = position; + /// + /// The position of the last appended record in the transaction. + /// + public readonly long Position = position; + + /// + /// The collection of responses for each stream appended in the transaction. + /// public readonly IEnumerable? Responses = responses; } From afa261dbf5c572ac88e20c671656b6a5aee65bc2 Mon Sep 17 00:00:00 2001 From: William Chong Date: Fri, 10 Oct 2025 15:08:12 +0400 Subject: [PATCH 24/29] Update protocols --- .../Core/proto/kurrent/rpc/errors.proto | 152 +++++++++++++++++ .../protocol/v2 => kurrent/rpc}/rpc.proto | 23 +-- .../proto/kurrentdb/protocol/v2/errors.proto | 147 ---------------- .../protocol/v2/streams/errors.proto | 158 ++++++++++++++---- .../protocol/v2/streams/streams.proto | 149 ++++++++++------- src/KurrentDB.Client/KurrentDB.Client.csproj | 10 ++ .../Streams/KurrentDBClient.MultiAppend.cs | 1 - 7 files changed, 379 insertions(+), 261 deletions(-) create mode 100644 src/KurrentDB.Client/Core/proto/kurrent/rpc/errors.proto rename src/KurrentDB.Client/Core/proto/{kurrentdb/protocol/v2 => kurrent/rpc}/rpc.proto (74%) delete mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/errors.proto diff --git a/src/KurrentDB.Client/Core/proto/kurrent/rpc/errors.proto b/src/KurrentDB.Client/Core/proto/kurrent/rpc/errors.proto new file mode 100644 index 000000000..bc2c3cdbc --- /dev/null +++ b/src/KurrentDB.Client/Core/proto/kurrent/rpc/errors.proto @@ -0,0 +1,152 @@ +// ****************************************************************************************** +// This protocol is UNSTABLE in the sense of being subject to change. +// ****************************************************************************************** + +syntax = "proto3"; + +package kurrent.rpc; + +option csharp_namespace = "Kurrent.Rpc"; + +import "kurrent/rpc/rpc.proto"; + +// The canonical server error codes for the Kurrent Platform gRPC APIs. +// These errors represent common failure modes across all Kurrent services. +enum ServerError { + // Default value. This value is not used. + // An error code MUST always be set to a non-zero value. + // If an error code is not explicitly set, it MUST be treated as + // an internal server error (INTERNAL). + UNSPECIFIED = 0; + + // Authentication or authorization failure. + // The client lacks valid credentials or sufficient permissions to perform the requested operation. + // + // Common causes: + // - Missing or invalid authentication tokens + // - Insufficient permissions for the operation + // - Expired credentials + // + // Client action: Check credentials, verify permissions, and re-authenticate if necessary. + // Not retriable without fixing the underlying authorization issue. + SERVER_ERROR_ACCESS_DENIED = 1 [(kurrent.rpc.error) = { + status_code: PERMISSION_DENIED, + has_details: true + }]; + + // The request is malformed or contains invalid data. + // The server cannot process the request due to client error. + // + // Common causes: + // - Invalid field values (e.g., empty required fields, out-of-range numbers) + // - Malformed data formats + // - Validation failures + // + // Client action: Fix the request data and retry. + // Not retriable without modifying the request. + SERVER_ERROR_BAD_REQUEST = 2 [(kurrent.rpc.error) = { + status_code: INVALID_ARGUMENT, + has_details: true + }]; + + // The server is not the cluster leader and cannot process write operations. + // In a clustered deployment, only the leader node can accept write operations. + // + // Common causes: + // - Client connected to a follower node + // - Leader election in progress + // - Network partition + // + // Client action: Redirect the request to the leader node indicated in the error details. + // Retriable after redirecting to the correct leader node. + SERVER_ERROR_NOT_LEADER_NODE = 5 [(kurrent.rpc.error) = { + status_code: FAILED_PRECONDITION, + has_details: true + }]; + + // The operation did not complete within the configured timeout period. + // + // Common causes: + // - Slow disk I/O during writes + // - Cluster consensus delays + // - Network latency + // - Heavy server load + // + // Client action: Retry with exponential backoff. Consider increasing timeout values. + // Retriable - the operation may succeed on retry. + SERVER_ERROR_OPERATION_TIMEOUT = 6 [(kurrent.rpc.error) = { + status_code: DEADLINE_EXCEEDED + }]; + + // The server is starting up or shutting down and cannot process requests. + // + // Common causes: + // - Server is initializing (loading indexes, recovering state) + // - Server is performing graceful shutdown + // - Server is performing maintenance operations + // + // Client action: Retry with exponential backoff. Wait for server to become ready. + // Retriable - the server will become available after initialization completes. + SERVER_ERROR_SERVER_NOT_READY = 7 [(kurrent.rpc.error) = { + status_code: UNAVAILABLE + }]; + + // The server is temporarily overloaded and cannot accept more requests. + // This is a backpressure mechanism to prevent server overload. + // + // Common causes: + // - Too many concurrent requests + // - Resource exhaustion (CPU, memory, disk I/O) + // - Rate limiting triggered + // + // Client action: Retry with exponential backoff. Reduce request rate. + // Retriable - the server may accept requests after load decreases. + SERVER_ERROR_SERVER_OVERLOADED = 8 [(kurrent.rpc.error) = { + status_code: UNAVAILABLE + }]; + + // An internal server error occurred. + // This indicates a bug or unexpected condition in the server. + // + // Common causes: + // - Unhandled exceptions + // - Assertion failures + // - Corrupted internal state + // - Programming errors + // + // Client action: Report to server administrators with request details. + // May be retriable, but likely indicates a server-side issue requiring investigation. + SERVER_ERROR_SERVER_MALFUNCTION = 9 [(kurrent.rpc.error) = { + status_code: INTERNAL + }]; +} + +// Details for ACCESS_DENIED errors. +message AccessDeniedErrorDetails { + // The friendly name of the operation that was denied. + string operation = 1; + + // The username of the user who was denied access. + optional string username = 2; + + // The permission that was required for this operation. + optional string permission = 3; +} + +// Details for NOT_LEADER_NODE errors. +message NotLeaderNodeErrorDetails { + // Information about the current cluster leader node. + NodeInfo current_leader = 1; + + // Information about a cluster node. + message NodeInfo { + // The hostname or IP address of the node. + string host = 1; + + // The gRPC port of the node. + int32 port = 2; + + // The unique instance ID of the node. + optional string node_id = 3; + } +} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/rpc.proto b/src/KurrentDB.Client/Core/proto/kurrent/rpc/rpc.proto similarity index 74% rename from src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/rpc.proto rename to src/KurrentDB.Client/Core/proto/kurrent/rpc/rpc.proto index 21382b950..73145fd06 100644 --- a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/rpc.proto +++ b/src/KurrentDB.Client/Core/proto/kurrent/rpc/rpc.proto @@ -53,11 +53,11 @@ message ErrorMetadata { // Indicates whether this error supports rich, typed detail messages. // Defaults to false (simple message string only). // The message type name must be derived from the enum name by convention. - // Mask: {EnumValue}ErrorDetails + // Mask: {EnumValue}ErrorDetails, {EnumValue}Error, {EnumValue} // // Examples: - // ACCESS_DENIED -> "AccessDeniedErrorDetails" - // SERVER_NOT_READY -> "ServerNotReadyErrorDetails" + // ACCESS_DENIED -> "AccessDeniedErrorDetails", "AccessDeniedError" or "AccessDenied" + // SERVER_NOT_READY -> "ServerNotReadyErrorDetails", "ServerNotReadyError" or "ServerNotReady" // // Code generators use the message type name to: // - Validate that the detail message matches the expected type @@ -72,20 +72,3 @@ extend google.protobuf.EnumValueOptions { // code generation and documentation. optional ErrorMetadata error = 50000; } - -// The top-level error message that must be returned by any service or operation -// in the Kurrent platform. -message RequestErrorInfo { - // The code must match one of the defined enum error codes from the module - // where the error originated from. - // A machine-readable error code that indicates the specific error condition. - // This should be at most 63 characters and match a regular expression of - // `[A-Z][A-Z0-9_]+[A-Z0-9]`, which represents UPPER_SNAKE_CASE. - // By convention, it will be generated from the enum value name if not - // explicitly specified. - // Conventions: - // - Prefix with the service name or domain to avoid collisions - // - Use UPPER_SNAKE_CASE with only letters, numbers, and underscores - // - Avoid redundant information (e.g., do not include "ERROR" suffix) - string code = 1; -} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/errors.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/errors.proto deleted file mode 100644 index f88f09b73..000000000 --- a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/errors.proto +++ /dev/null @@ -1,147 +0,0 @@ -// ****************************************************************************************** -// This protocol is UNSTABLE in the sense of being subject to change. -// ****************************************************************************************** - -syntax = "proto3"; - -package kurrentdb.protocol.v2.common.errors; - -option csharp_namespace = "KurrentDB.Protocol.V2.Common.Errors"; - -import "rpc.proto"; - -enum CommonError { - // Default value. This value is not used. - // An error code MUST always be set to a non-zero value. - // If an error code is not explicitly set, it MUST be treated as - // an internal server error (INTERNAL). - UNSPECIFIED = 0; - - ACCESS_DENIED = 1 [(kurrent.rpc.error) = { - status_code: PERMISSION_DENIED, - has_details: true - }]; - - INVALID_REQUEST = 2 [(kurrent.rpc.error) = { - status_code: INVALID_ARGUMENT, - has_details: true - }]; - - NOT_LEADER_NODE = 5 [(kurrent.rpc.error) = { - status_code: FAILED_PRECONDITION, - has_details: true - }]; - - OPERATION_TIMEOUT = 6 [(kurrent.rpc.error) = { - status_code: DEADLINE_EXCEEDED - }]; - - SERVER_NOT_READY = 7 [(kurrent.rpc.error) = { - status_code: UNAVAILABLE - }]; - - SERVER_OVERLOADED = 8 [(kurrent.rpc.error) = { - status_code: UNAVAILABLE - }]; - - SERVER_MALFUNCTION = 9 [(kurrent.rpc.error) = { - status_code: INTERNAL - }]; - -// // The operation was aborted, typically due to a concurrency issue such as a -// // sequencer conflict or transaction abort. -// // This error will only be used when there is no intention to create a dedicated -// // error code for the specific issue, perhaps because the issue is too generic -// // or too transient or temporary in terms of handling. -// OPERATION_ABORTED = 10 [(kurrent.rpc.error) = { -// status_code: ABORTED -// }]; -} - -message AccessDeniedErrorDetails { - // The scope in which access was denied. - // It could represent a resource, a domain, a permission type - // or a "path" that is a combination of these. - // (e.g., "stream:orders", "db:customers:read", etc.) - optional string scope = 1; - - // The username of the user who was denied access. - optional string username = 2; -} - -message InvalidRequestErrorDetails { - // Detailed information about each invalid argument. - repeated FieldViolation violations = 1; - - // Describes a single field violation. - message FieldViolation { - // A path that leads to a field in the request body. The value will be a - // sequence of dot-separated identifiers that identify a protocol buffer - // field. - // - // Consider the following: - // - // message CreateContactRequest { - // message EmailAddress { - // enum Type { - // TYPE_UNSPECIFIED = 0; - // HOME = 1; - // WORK = 2; - // } - // - // optional string email = 1; - // repeated EmailType type = 2; - // } - // - // string full_name = 1; - // repeated EmailAddress email_addresses = 2; - // } - // - // In this example, in proto `field` could take one of the following values: - // - // * `full_name` for a violation in the `full_name` value - // * `email_addresses[1].email` for a violation in the `email` field of the - // first `email_addresses` message - // * `email_addresses[3].type[2]` for a violation in the second `type` - // value in the third `email_addresses` message. - // - // In JSON, the same values are represented as: - // - // * `fullName` for a violation in the `fullName` value - // * `emailAddresses[1].email` for a violation in the `email` field of the - // first `emailAddresses` message - // * `emailAddresses[3].type[2]` for a violation in the second `type` - // value in the third `emailAddresses` message. - string field = 1; - - // A description of why the request element is bad. - string description = 2; - } -} - -message NotLeaderNodeErrorDetails { - // The host of the current leader node - string host = 1; - - // The port of the current leader node - int32 port = 2; - - // The instance ID of the current leader node - optional string node_id = 3; -} - -message RetryInfoErrorDetails { - // The duration in milliseconds after which the client can retry the operation. - int32 retry_delay_ms = 1; -} - -message NodeInfoErrorDetails { - // The host of the node - string host = 1; - - // The port of the node - int32 port = 2; - - // The instance ID of the node - optional string node_id = 3; -} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/errors.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/errors.proto index 260bf8f59..5cb36f95b 100644 --- a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/errors.proto +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/errors.proto @@ -8,120 +8,206 @@ package kurrentdb.protocol.v2.streams.errors; option csharp_namespace = "KurrentDB.Protocol.V2.Streams.Errors"; -import "rpc.proto"; +import "kurrent/rpc/rpc.proto"; +// Error codes specific to the Streams API. +// These errors represent failure modes when working with streams of records. enum StreamsError { // Default value. This value is not used. // An error code MUST always be set to a non-zero value. // If an error code is not explicitly set, it MUST be treated as // an internal server error (INTERNAL). - UNSPECIFIED = 0; - - // The stream was not found. - // This is recoverable by the client by creating the stream first. - STREAM_NOT_FOUND = 1 [(kurrent.rpc.error) = { + STREAMS_ERROR_UNSPECIFIED = 0; + + // The requested stream does not exist in the database. + // + // Common causes: + // - Stream name typo or incorrect stream identifier + // - Stream was never created (no events appended yet) + // - Stream was deleted and not yet recreated + // + // Client action: Verify the stream name is correct. Create the stream by appending to it. + // Recoverable by creating the stream first (append with NO_STREAM expected revision). + STREAMS_ERROR_STREAM_NOT_FOUND = 1 [(kurrent.rpc.error) = { status_code: NOT_FOUND, has_details: true, }]; - // The stream already exists. - // This is recoverable by the client by using the existing stream. - STREAM_ALREADY_EXISTS = 2 [(kurrent.rpc.error) = { + // The stream already exists when an operation expected it not to exist. + // + // Common causes: + // - Attempting to create a stream that already has events + // - Using NO_STREAM expected revision on an existing stream + // - Race condition with concurrent stream creation + // + // Client action: Use the existing stream or use a different expected revision. + // Recoverable by adjusting the expected revision or using the existing stream. + STREAMS_ERROR_STREAM_ALREADY_EXISTS = 2 [(kurrent.rpc.error) = { status_code: ALREADY_EXISTS, has_details: true }]; // The stream has been soft deleted. - // It will not be visible in the stream list, until it is restored by appending to it again. - STREAM_DELETED = 3 [(kurrent.rpc.error) = { + // Soft-deleted streams are hidden from stream lists but can be restored by appending to them. + // + // Common causes: + // - Stream was explicitly soft-deleted via delete operation + // - Attempting to read from a soft-deleted stream + // + // Client action: Restore the stream by appending new events, or accept that the stream is deleted. + // Recoverable by appending to the stream to restore it. + STREAMS_ERROR_STREAM_DELETED = 3 [(kurrent.rpc.error) = { status_code: FAILED_PRECONDITION, has_details: true }]; - // The stream has been tombstoned. - // It has been permanently removed from the system and cannot be restored. - STREAM_TOMBSTONED = 4 [(kurrent.rpc.error) = { + // The stream has been tombstoned (permanently deleted). + // Tombstoned streams cannot be restored and will never accept new events. + // + // Common causes: + // - Stream was explicitly tombstoned via tombstone operation + // - Administrative deletion of sensitive data + // - Attempting to write to or read from a tombstoned stream + // + // Client action: Stream is permanently removed. Create a new stream with a different name if needed. + // Not recoverable - the stream cannot be restored. + STREAMS_ERROR_STREAM_TOMBSTONED = 4 [(kurrent.rpc.error) = { status_code: FAILED_PRECONDITION, has_details: true }]; - // The expected revision of the stream does not match the actual revision. - // This is recoverable by the client by fetching the current revision and retrying. - STREAM_REVISION_CONFLICT = 5 [(kurrent.rpc.error) = { + // The expected revision does not match the actual stream revision. + // This is an optimistic concurrency control failure. + // + // Common causes: + // - Another client modified the stream concurrently + // - Client has stale state about the stream revision + // - Race condition in distributed system + // + // Client action: Fetch the current stream revision and retry with the correct expected revision. + // Recoverable by reading the current state and retrying with proper optimistic concurrency control. + STREAMS_ERROR_STREAM_REVISION_CONFLICT = 5 [(kurrent.rpc.error) = { status_code: FAILED_PRECONDITION, has_details: true }]; - // The size of a record being appended exceeds the maximum allowed size. - // It is recoverable by the client by sending a smaller record. - APPEND_RECORD_SIZE_EXCEEDED = 6 [(kurrent.rpc.error) = { + // A single record being appended exceeds the maximum allowed size. + // + // Common causes: + // - Record payload is too large (exceeds server's max record size configuration) + // - Excessive metadata in properties + // - Large binary data without chunking + // + // Client action: Reduce record size, split large payloads across multiple records, or increase server limits. + // Recoverable by reducing record size or adjusting server configuration. + STREAMS_ERROR_APPEND_RECORD_SIZE_EXCEEDED = 6 [(kurrent.rpc.error) = { status_code: INVALID_ARGUMENT, has_details: true }]; - // When the transaction exceeds the maximum size allowed (max chunk size). - // It is recoverable by the client by sending a smaller transaction. - APPEND_TRANSACTION_SIZE_EXCEEDED = 7 [(kurrent.rpc.error) = { + // The total size of all records in a single append session exceeds the maximum allowed transaction size. + // + // Common causes: + // - Too many records in a single append session + // - Combined payload size exceeds server's max transaction size + // - Attempting to write very large batches + // + // Client action: Split the append into multiple smaller transactions. + // Recoverable by reducing the number of records per append session. + STREAMS_ERROR_APPEND_TRANSACTION_SIZE_EXCEEDED = 7 [(kurrent.rpc.error) = { status_code: ABORTED, has_details: true }]; - // The stream is already in an append session. - // Appending to the same stream multiple times is currently not supported. - STREAM_ALREADY_IN_APPEND_SESSION = 8 [(kurrent.rpc.error) = { + // The same stream appears multiple times in a single append session. + // This is currently not supported to prevent complexity with expected revisions and ordering. + // + // Common causes: + // - Accidentally appending to the same stream twice in one session + // - Application logic error in batch operations + // + // Client action: Remove duplicate streams from the append session or split into multiple sessions. + // Recoverable by restructuring the append session to reference each stream only once. + STREAMS_ERROR_STREAM_ALREADY_IN_APPEND_SESSION = 8 [(kurrent.rpc.error) = { status_code: ABORTED, has_details: true }]; + + // An append session was started but no append requests were sent before completing the stream. + // + // Common causes: + // - Client completed the stream without sending any AppendRequest messages + // - Application logic error + // + // Client action: Ensure at least one AppendRequest is sent before completing the stream. + // Recoverable by properly implementing the append session protocol. + STREAMS_ERROR_APPEND_SESSION_NO_REQUESTS = 9 [(kurrent.rpc.error) = { + status_code: FAILED_PRECONDITION + }]; } +// Details for STREAM_NOT_FOUND errors. message StreamNotFoundErrorDetails { // The name of the stream that was not found. string stream = 1; } +// Details for STREAM_ALREADY_EXISTS errors. message StreamAlreadyExistsErrorDetails { // The name of the stream that already exists. string stream = 1; } +// Details for STREAM_DELETED errors. message StreamDeletedErrorDetails { // The name of the stream that was deleted. string stream = 1; } +// Details for STREAM_TOMBSTONED errors. message StreamTombstonedErrorDetails { // The name of the stream that was tombstoned. string stream = 1; } +// Details for STREAM_REVISION_CONFLICT errors. message StreamRevisionConflictErrorDetails { // The name of the stream that had a revision conflict. string stream = 1; - // The actual revision of the stream. - int64 expected_revision = 2; - // The actual revision of the stream. - int64 actual_revision = 3; + + // The expected revision that was provided in the append request. + sint64 expected_revision = 2; + + // The actual current revision of the stream. + sint64 actual_revision = 3; } +// Details for APPEND_RECORD_SIZE_EXCEEDED errors. message AppendRecordSizeExceededErrorDetails { // The name of the stream where the append was attempted. string stream = 1; - // The identifier of the offending and oversized record. + + // The identifier of the record that exceeded the size limit. string record_id = 2; - // The size of the huge record in bytes. + + // The actual size of the record in bytes. int32 size = 3; - // The maximum allowed size of a single record that can be appended in bytes. + + // The maximum allowed size of a single record in bytes. int32 max_size = 4; } +// Details for APPEND_TRANSACTION_SIZE_EXCEEDED errors. message AppendTransactionSizeExceededErrorDetails { - // The size of the huge transaction in bytes. + // The actual size of the transaction in bytes. int32 size = 1; - // The maximum allowed size of the append transaction in bytes. + + // The maximum allowed size of an append transaction in bytes. int32 max_size = 2; } +// Details for STREAM_ALREADY_IN_APPEND_SESSION errors. message StreamAlreadyInAppendSessionErrorDetails { - // The name of the stream that is already in an append session. + // The name of the stream that appears multiple times. string stream = 1; } diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto index 99803559e..3592c60b1 100644 --- a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto @@ -11,47 +11,64 @@ option csharp_namespace = "KurrentDB.Protocol.V2.Streams"; import "google/protobuf/struct.proto"; service StreamsService { - // // Executes an atomic operation to append records to multiple streams. - // // This transactional method ensures that all appends either succeed - // // completely, or are entirely rolled back, thereby maintaining strict - // // data consistency across all involved streams. - // rpc Append(AppendRequest) returns (AppendResponse); - - // Streaming version of Append that allows clients to send multiple - // append requests continuously. Once completed, all records are - // appended transactionally (all succeed or fail together). - // Provides improved efficiency for high-throughput scenarios while - // maintaining the same transactional guarantees. + // Appends records to multiple streams atomically within a single transaction. + // + // This is a client-streaming RPC where the client sends multiple AppendRequest messages + // (one per stream) and receives a single AppendSessionResponse upon commit. + // + // Guarantees: + // - Atomicity: All writes succeed or all fail together + // - Optimistic Concurrency: Expected revisions are validated for all streams before commit + // - Ordering: Records within each stream maintain send order + // + // Current Limitations: + // - Each stream can only appear once per session (no multiple appends to same stream) + // + // Example flow: + // 1. Client opens stream + // 2. Client sends AppendRequest for stream "orders" with 3 records + // 3. Client sends AppendRequest for stream "inventory" with 2 records + // 4. Client completes the stream + // 5. Server validates, commits, returns AppendSessionResponse with positions rpc AppendSession(stream AppendRequest) returns (AppendSessionResponse); } -message AppendSessionResponse { - repeated AppendResponse output = 1; - - // The position of the last appended record in the transaction. - int64 position = 2; -} - // Represents the input for appending records to a specific stream. message AppendRequest { - // The name of the stream to append records to. + // The stream to append records to. string stream = 1; + // The records to append to the stream. repeated AppendRecord records = 2; - // The expected revision of the stream. If the stream's current revision does - // not match, the append will fail. - // The expected revision can also be one of the special values - // from ExpectedRevisionConstants. - // Missing value means no expectation, the same as EXPECTED_REVISION_CONSTANTS_ANY + + // The expected revision for optimistic concurrency control. + // Can be either: + // - A specific revision number (0, 1, 2, ...) - the stream must be at exactly this revision + // - An ExpectedRevisionConstants value (-4, -2, -1) for special semantics + // + // If omitted, defaults to EXPECTED_REVISION_CONSTANTS_ANY (-2). optional sint64 expected_revision = 3; } // Represents the outcome of an append operation. message AppendResponse { - // The name of the stream to which records were appended. + // The stream to which records were appended. string stream = 1; - // The expected revision of the stream after the append operation. - int64 stream_revision = 2; + + // The actual/current revision of the stream after the append. + // This is the revision number of the last record written to this stream. + sint64 stream_revision = 2; + + // The position of the last appended record in the global log. + optional sint64 position = 3; +} + +message AppendSessionResponse { + // The results of each append request in the session. + repeated AppendResponse output = 1; + + // The global commit position of the last appended record in the session. + sint64 position = 2; } // Represents the data format of the schema. @@ -64,11 +81,20 @@ enum SchemaFormat { SCHEMA_FORMAT_BYTES = 4; } +// Schema information for record validation and interpretation. message SchemaInfo { - // The format of the schema that the record conforms to. - SchemaFormat format = 1; - - // The name of the schema that the record conforms to. + // The format of the data payload. + // Determines how the bytes in AppendRecord.data should be interpreted. + SchemaFormat format = 1; + + // The schema name (replaces the legacy "event type" concept). + // Identifies what kind of data this record contains. + // + // Common naming formats: + // - Kebab-case: "order-placed", "customer-registered" + // - URN format: "urn:kurrentdb:events:order-placed:v1" + // - Dotted namespace: "Teams.Player.V1", "Orders.OrderPlaced.V2" + // - Reverse domain: "com.acme.orders.placed" string name = 2; // The identifier of the specific version of the schema that the record payload @@ -79,42 +105,51 @@ message SchemaInfo { // Record to be appended to a stream. message AppendRecord { - // Universally Unique identifier for the record. Must be a guid. + // Unique identifier for this record (must be a valid UUID/GUID). // If not provided, the server will generate a new one. optional string record_id = 1; - // The timestamp of when the record was created, represented as - // milliseconds since the Unix epoch. This is primarily for - // informational purposes and does not affect the ordering of records - // within the stream, which is determined by the server. - // If not provided, the server will assign it upon receipt. - optional int64 timestamp = 2; - // A collection of properties providing additional information about the - // record. This can include user-defined metadata or system properties. - // System properties are uniquely identified by the "$." prefix. - map properties = 3; - - // Information about the schema that the record payload conforms to. - SchemaInfo schema = 4; - - // The actual data payload of the record. - bytes data = 5; + // record. Can contain user-defined or system propreties. + // System keys will be prefixed with "$" (e.g., "$timestamp"). + // User-defined keys MUST NOT start with "$". + // + // Common examples: + // User metadata: + // - "user-id": "12345" + // - "tenant": "acme-corp" + // - "source": "mobile-app" + // + // System metadata (with $ prefix): + // - "$trace-id": "4bf92f3577b34da6a3ce929d0e0e4736" // OpenTelemetry trace ID + // - "$span-id": "00f067aa0ba902b7" // OpenTelemetry span ID + // - "$timestamp": "2025-01-15T10:30:00.000Z" // ISO 8601 timestamp + map properties = 2; + + // Schema information for this record. + SchemaInfo schema = 3; + + // The record payload as raw bytes. + // The format specified in SchemaInfo determines how to interpret these bytes. + bytes data = 4; } -// Constants that match the expected state of a stream during an -// append operation. It can be used to specify whether the stream should exist, -// not exist, or can be in any state. +// Constants for expected revision validation in optimistic concurrency control. +// These can be used in the expected_revision field, or you can specify an actual revision number. enum ExpectedRevisionConstants { - // The stream should exist and have a single event. + // The stream must have exactly one event at revision 0. + // Used for scenarios requiring strict single-event semantics. EXPECTED_REVISION_CONSTANTS_SINGLE_EVENT = 0; - // It is not important whether the stream exists or not. - EXPECTED_REVISION_CONSTANTS_ANY = -2; - - // The stream should not exist. If it does, the append will fail. + // The stream must not exist yet (first write to the stream). + // Fails if the stream already has events. EXPECTED_REVISION_CONSTANTS_NO_STREAM = -1; - // The stream should exist + // Accept any current state of the stream (no optimistic concurrency check). + // The write will succeed regardless of the stream's current revision. + EXPECTED_REVISION_CONSTANTS_ANY = -2; + + // The stream must exist (have at least one record). + // Fails if the stream doesn't exist yet. EXPECTED_REVISION_CONSTANTS_EXISTS = -4; } diff --git a/src/KurrentDB.Client/KurrentDB.Client.csproj b/src/KurrentDB.Client/KurrentDB.Client.csproj index b16474476..3cf633376 100644 --- a/src/KurrentDB.Client/KurrentDB.Client.csproj +++ b/src/KurrentDB.Client/KurrentDB.Client.csproj @@ -44,6 +44,16 @@ Access="internal" AdditionalImportDirs="Core/proto;$(PkgGoogle_Api_CommonProtos)\content\protos" /> + + + diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs index 81f094f56..9b4932a20 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs @@ -11,7 +11,6 @@ using KurrentDB.Diagnostics; using KurrentDB.Diagnostics.Telemetry; using KurrentDB.Protocol.V2.Streams; -using KurrentDB.Protocol.V2.Streams.Errors; using static KurrentDB.Diagnostics.Tracing.TracingConstants; using static KurrentDB.Protocol.V2.Streams.StreamsService; From 8d13ff0a7c5db043d2a02d1cfe4be7c18b794086 Mon Sep 17 00:00:00 2001 From: William Chong Date: Fri, 10 Oct 2025 17:21:52 +0400 Subject: [PATCH 25/29] Reduce code --- ...pendTransactionMaxSizeExceededException.cs | 33 ++++++++++ .../Core/Exceptions/StreamDeletedException.cs | 10 ++++ .../TransactionMaxSizeExceededException.cs | 20 ------- .../WrongExpectedVersionException.cs | 13 ++++ .../Streams/KurrentDBClient.MultiAppend.cs | 13 +++- .../MaximumAppendSizeExceededException.cs | 14 +++++ .../Streams/Model/StreamsClientMapper.cs | 60 ------------------- .../GlobalEnvironment.cs | 5 +- 8 files changed, 85 insertions(+), 83 deletions(-) create mode 100644 src/KurrentDB.Client/Core/Exceptions/AppendTransactionMaxSizeExceededException.cs delete mode 100644 src/KurrentDB.Client/Core/Exceptions/TransactionMaxSizeExceededException.cs diff --git a/src/KurrentDB.Client/Core/Exceptions/AppendTransactionMaxSizeExceededException.cs b/src/KurrentDB.Client/Core/Exceptions/AppendTransactionMaxSizeExceededException.cs new file mode 100644 index 000000000..1481b23c5 --- /dev/null +++ b/src/KurrentDB.Client/Core/Exceptions/AppendTransactionMaxSizeExceededException.cs @@ -0,0 +1,33 @@ +using Grpc.Core; +using KurrentDB.Protocol.V2.Streams.Errors; + +namespace KurrentDB.Client; + +/// +/// Exception thrown when a transaction exceeds the allowed maximum size limit. +/// +public class AppendTransactionMaxSizeExceededException(int size, int maxSize, Exception? innerException = null) + : Exception( + $"The total size of the append transaction ({size}) exceeds the maximum allowed size of {maxSize} bytes by {size - maxSize}", + innerException + ) { + /// + /// The size of the huge transaction in bytes. + /// + public int Size { get; } = size; + + /// + /// The maximum allowed size of the append transaction in bytes. + /// + public int MaxSize { get; } = maxSize; + + public static AppendTransactionMaxSizeExceededException FromRpcException(RpcException ex) => FromRpcStatus(ex.GetRpcStatus()!); + + public static AppendTransactionMaxSizeExceededException FromRpcStatus(Google.Rpc.Status ex) { + var details = ex.GetDetail(); + return new AppendTransactionMaxSizeExceededException( + details.Size, + details.MaxSize + ); + } +} diff --git a/src/KurrentDB.Client/Core/Exceptions/StreamDeletedException.cs b/src/KurrentDB.Client/Core/Exceptions/StreamDeletedException.cs index 38cde3f94..43c29a2c0 100644 --- a/src/KurrentDB.Client/Core/Exceptions/StreamDeletedException.cs +++ b/src/KurrentDB.Client/Core/Exceptions/StreamDeletedException.cs @@ -1,3 +1,6 @@ +using Grpc.Core; +using KurrentDB.Protocol.V2.Streams.Errors; + namespace KurrentDB.Client { /// /// Exception thrown if an operation is attempted on a stream which @@ -18,5 +21,12 @@ public StreamDeletedException(string stream, Exception? exception = null) : base($"Event stream '{stream}' is deleted.", exception) { Stream = stream; } + + public static StreamDeletedException FromRpcException(RpcException ex) => FromRpcStatus(ex.GetRpcStatus()!); + + public static StreamDeletedException FromRpcStatus(Google.Rpc.Status ex) { + var details = ex.GetDetail(); + return new StreamDeletedException(details.Stream); + } } } diff --git a/src/KurrentDB.Client/Core/Exceptions/TransactionMaxSizeExceededException.cs b/src/KurrentDB.Client/Core/Exceptions/TransactionMaxSizeExceededException.cs deleted file mode 100644 index f54fc9826..000000000 --- a/src/KurrentDB.Client/Core/Exceptions/TransactionMaxSizeExceededException.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace KurrentDB.Client; - -/// -/// Exception thrown when a transaction exceeds the allowed maximum size limit. -/// -public class TransactionMaxSizeExceededException(int size, int maxSize, Exception? innerException = null) - : Exception( - $"The total size of the append transaction ({size}) exceeds the maximum allowed size of {maxSize} bytes by {size - maxSize}", - innerException - ) { - /// - /// The size of the huge transaction in bytes. - /// - public int Size { get; } = size; - - /// - /// The maximum allowed size of the append transaction in bytes. - /// - public int MaxSize { get; } = maxSize; -} diff --git a/src/KurrentDB.Client/Core/Exceptions/WrongExpectedVersionException.cs b/src/KurrentDB.Client/Core/Exceptions/WrongExpectedVersionException.cs index 0736dc84c..db6c7c391 100644 --- a/src/KurrentDB.Client/Core/Exceptions/WrongExpectedVersionException.cs +++ b/src/KurrentDB.Client/Core/Exceptions/WrongExpectedVersionException.cs @@ -1,4 +1,6 @@ using System; +using Grpc.Core; +using KurrentDB.Protocol.V2.Streams.Errors; namespace KurrentDB.Client { /// @@ -45,5 +47,16 @@ public WrongExpectedVersionException(string streamName, StreamState expectedStre ExpectedVersion = expectedStreamState.ToInt64(); ActualVersion = actualStreamState.ToInt64(); } + + public static WrongExpectedVersionException FromRpcException(RpcException ex) => FromRpcStatus(ex.GetRpcStatus()!); + + public static WrongExpectedVersionException FromRpcStatus(Google.Rpc.Status ex) { + var details = ex.GetDetail(); + return new WrongExpectedVersionException( + details.Stream, + StreamState.StreamRevision((ulong)details.ExpectedRevision), + StreamState.StreamRevision((ulong)details.ActualRevision) + ); + } } } diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs index 9b4932a20..ce2d05c70 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs @@ -6,6 +6,7 @@ #pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). using System.Diagnostics; +using Google.Rpc; using Grpc.Core; using KurrentDB.Client.Diagnostics; using KurrentDB.Diagnostics; @@ -26,7 +27,7 @@ public partial class KurrentDBClient { /// A task that represents the asynchronous operation, with a result of type , indicating the outcome of the operation. /// /// On success, returns containing the successful append results. - /// , , , or . + /// , , , or . /// /// /// Thrown if the server does not support multi-stream append functionality (requires server version 25.1 or higher). @@ -78,7 +79,15 @@ await session.RequestStream return new MultiStreamAppendResponse(response.Position, responses); } catch (RpcException ex) { - throw ex.MapRpcException(); + var status = ex.GetRpcStatus()!; + + throw status.GetDetail() switch { + { Reason: "STREAM_REVISION_CONFLICT" } => WrongExpectedVersionException.FromRpcException(ex), + { Reason: "STREAM_TOMBSTONED" } => StreamDeletedException.FromRpcException(ex), + { Reason: "APPEND_RECORD_SIZE_EXCEEDED" } => AppendRecordSizeExceededException.FromRpcException(ex), + { Reason: "APPEND_TRANSACTION_SIZE_EXCEEDED" } => AppendTransactionMaxSizeExceededException.FromRpcException(ex), + _ => ex + }; } } } diff --git a/src/KurrentDB.Client/Streams/MaximumAppendSizeExceededException.cs b/src/KurrentDB.Client/Streams/MaximumAppendSizeExceededException.cs index 07e59ad04..3ec0dab13 100644 --- a/src/KurrentDB.Client/Streams/MaximumAppendSizeExceededException.cs +++ b/src/KurrentDB.Client/Streams/MaximumAppendSizeExceededException.cs @@ -1,4 +1,6 @@ using System; +using Grpc.Core; +using KurrentDB.Protocol.V2.Streams.Errors; namespace KurrentDB.Client { /// @@ -40,5 +42,17 @@ public AppendRecordSizeExceededException(string stream, string recordId, long si Size = size; MaxSize = maxSize; } + + public static AppendRecordSizeExceededException FromRpcException(RpcException ex) => FromRpcStatus(ex.GetRpcStatus()!); + + public static AppendRecordSizeExceededException FromRpcStatus(Google.Rpc.Status ex) { + var details = ex.GetDetail(); + return new AppendRecordSizeExceededException( + details.Stream, + details.RecordId, + details.Size, + details.MaxSize + ); + } } } diff --git a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs index ea28a3af0..ea8a44d92 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs @@ -1,13 +1,7 @@ -// ReSharper disable InconsistentNaming - -#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). - using System.Diagnostics; using Google.Protobuf; -using Grpc.Core; using KurrentDB.Client.Diagnostics; using KurrentDB.Protocol.V2.Streams; -using KurrentDB.Protocol.V2.Streams.Errors; using static KurrentDB.Client.Constants.Metadata; using SchemaFormat = KurrentDB.Protocol.V2.Streams.SchemaFormat; @@ -43,58 +37,4 @@ public static ValueTask Map(this EventData source) { return new ValueTask(record); } - - public static AppendResponse Map(this KurrentDB.Protocol.V2.Streams.AppendResponse source) => new(source.Stream, source.StreamRevision); - - public static Exception MapRpcException(this RpcException ex) { - var status = ex.GetRpcStatus(); - - return ex.StatusCode switch { - StatusCode.Aborted => HandleAborted(ex, status), - StatusCode.FailedPrecondition => HandleFailedPrecondition(ex, status), - StatusCode.InvalidArgument => HandleInvalidArgument(ex, status), - _ => ex - }; - } - - static Exception HandleFailedPrecondition(RpcException ex, Google.Rpc.Status? status) { - var revisionConflict = status?.GetDetail(); - if (revisionConflict != null) { - return new WrongExpectedVersionException( - revisionConflict.Stream, - StreamState.StreamRevision((ulong)revisionConflict.ExpectedRevision), - StreamState.StreamRevision((ulong)revisionConflict.ActualRevision), - ex - ); - } - - var tombstoned = status?.GetDetail(); - if (tombstoned != null) return new StreamDeletedException(tombstoned.Stream, ex); - - return ex; - } - - static Exception HandleInvalidArgument(RpcException ex, Google.Rpc.Status? status) { - var recordSizeExceeded = status?.GetDetail(); - if (recordSizeExceeded != null) return new AppendRecordSizeExceededException( - recordSizeExceeded.Stream, - recordSizeExceeded.RecordId, - recordSizeExceeded.Size, - recordSizeExceeded.MaxSize, - ex - ); - - return ex; - } - - static Exception HandleAborted(RpcException ex, Google.Rpc.Status? status) { - var transactionSizeExceeded = status?.GetDetail(); - if (transactionSizeExceeded != null) return new TransactionMaxSizeExceededException( - transactionSizeExceeded.Size, - transactionSizeExceeded.MaxSize, - ex - ); - - return ex; - } } diff --git a/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs b/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs index 4f48787ba..401efcf20 100644 --- a/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs +++ b/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs @@ -23,7 +23,8 @@ static GlobalEnvironment() { static void EnsureDefaults(IConfiguration configuration) { // internal defaults - configuration.EnsureValue("TESTCONTAINER_KURRENTDB_IMAGE", "docker.cloudsmith.io/eventstore/kurrent-staging/kurrentdb:ci"); + // configuration.EnsureValue("TESTCONTAINER_KURRENTDB_IMAGE", "docker.cloudsmith.io/eventstore/kurrent-staging/kurrentdb:ci"); + configuration.EnsureValue("TESTCONTAINER_KURRENTDB_IMAGE", "docker.cloudsmith.io/eventstore/kurrent-preview/kurrentdb:25.1.0-rc.1-x64-8.0-jammy"); // database defaults configuration.EnsureValue("EVENTSTORE_TELEMETRY_OPTOUT", "true"); @@ -41,6 +42,8 @@ static void EnsureDefaults(IConfiguration configuration) { configuration.EnsureValue("EVENTSTORE_LOG_LEVEL", "Default"); configuration.EnsureValue("EVENTSTORE_DISABLE_LOG_FILE", "true"); configuration.EnsureValue("EVENTSTORE_MAX_APPEND_SIZE", $"{MaxAppendSize}"); + configuration.EnsureValue("EVENTSTORE_MAX_APPEND_EVENT_SIZE", $"{MaxAppendSize}"); + } } From 4e6abb5b9f529250d48a9424679ef92389325f5d Mon Sep 17 00:00:00 2001 From: William Chong Date: Mon, 20 Oct 2025 13:39:08 +0400 Subject: [PATCH 26/29] Change image to kurrentdb latest --- .../Core/Exceptions/AccessDeniedException.cs | 8 +++++- .../Core/Exceptions/NotLeaderException.cs | 13 ++++++++- .../Core/Exceptions/StreamDeletedException.cs | 10 ------- .../Exceptions/StreamTombstonedException.cs | 28 +++++++++++++++++++ .../Streams/KurrentDBClient.MultiAppend.cs | 3 +- .../GlobalEnvironment.cs | 3 +- .../Streams/MultiStreamAppendTests.cs | 2 +- 7 files changed, 50 insertions(+), 17 deletions(-) create mode 100644 src/KurrentDB.Client/Core/Exceptions/StreamTombstonedException.cs diff --git a/src/KurrentDB.Client/Core/Exceptions/AccessDeniedException.cs b/src/KurrentDB.Client/Core/Exceptions/AccessDeniedException.cs index 13ea71b2a..c2a34eb36 100644 --- a/src/KurrentDB.Client/Core/Exceptions/AccessDeniedException.cs +++ b/src/KurrentDB.Client/Core/Exceptions/AccessDeniedException.cs @@ -1,4 +1,4 @@ -using System; +using Grpc.Core; namespace KurrentDB.Client { /// @@ -18,5 +18,11 @@ public AccessDeniedException(string message, Exception innerException) : base(me public AccessDeniedException() : base("Access denied.") { } + + public static AccessDeniedException FromRpcException(RpcException ex) => FromRpcStatus(ex.GetRpcStatus()!); + + public static AccessDeniedException FromRpcStatus(Google.Rpc.Status ex) { + return new(ex.Message, ex.ToRpcException()); + } } } diff --git a/src/KurrentDB.Client/Core/Exceptions/NotLeaderException.cs b/src/KurrentDB.Client/Core/Exceptions/NotLeaderException.cs index f2e0e82b0..bcd853f6b 100644 --- a/src/KurrentDB.Client/Core/Exceptions/NotLeaderException.cs +++ b/src/KurrentDB.Client/Core/Exceptions/NotLeaderException.cs @@ -1,5 +1,6 @@ -using System; using System.Net; +using Grpc.Core; +using Kurrent.Rpc; namespace KurrentDB.Client { /// @@ -22,5 +23,15 @@ public NotLeaderException(string host, int port, Exception? exception = null) : $"Not leader. New leader at {host}:{port}.", exception) { LeaderEndpoint = new DnsEndPoint(host, port); } + + public static NotLeaderException FromRpcException(RpcException ex) => FromRpcStatus(ex.GetRpcStatus()!); + + public static NotLeaderException FromRpcStatus(Google.Rpc.Status ex) { + var details = ex.GetDetail(); + return new NotLeaderException( + details.CurrentLeader.Host, + details.CurrentLeader.Port + ); + } } } diff --git a/src/KurrentDB.Client/Core/Exceptions/StreamDeletedException.cs b/src/KurrentDB.Client/Core/Exceptions/StreamDeletedException.cs index 43c29a2c0..38cde3f94 100644 --- a/src/KurrentDB.Client/Core/Exceptions/StreamDeletedException.cs +++ b/src/KurrentDB.Client/Core/Exceptions/StreamDeletedException.cs @@ -1,6 +1,3 @@ -using Grpc.Core; -using KurrentDB.Protocol.V2.Streams.Errors; - namespace KurrentDB.Client { /// /// Exception thrown if an operation is attempted on a stream which @@ -21,12 +18,5 @@ public StreamDeletedException(string stream, Exception? exception = null) : base($"Event stream '{stream}' is deleted.", exception) { Stream = stream; } - - public static StreamDeletedException FromRpcException(RpcException ex) => FromRpcStatus(ex.GetRpcStatus()!); - - public static StreamDeletedException FromRpcStatus(Google.Rpc.Status ex) { - var details = ex.GetDetail(); - return new StreamDeletedException(details.Stream); - } } } diff --git a/src/KurrentDB.Client/Core/Exceptions/StreamTombstonedException.cs b/src/KurrentDB.Client/Core/Exceptions/StreamTombstonedException.cs new file mode 100644 index 000000000..6c9ad341a --- /dev/null +++ b/src/KurrentDB.Client/Core/Exceptions/StreamTombstonedException.cs @@ -0,0 +1,28 @@ +using Grpc.Core; +using KurrentDB.Protocol.V2.Streams.Errors; + +namespace KurrentDB.Client; + +public class StreamTombstonedException : Exception { + /// + /// The name of the tombstoned stream. + /// + public readonly string Stream; + + /// + /// Constructs a new instance of . + /// + /// The name of the tombstoned stream. + /// + public StreamTombstonedException(string stream, Exception? exception = null) + : base($"Event stream '{stream}' is tombstoned.", exception) { + Stream = stream; + } + + public static StreamTombstonedException FromRpcException(RpcException ex) => FromRpcStatus(ex.GetRpcStatus()!); + + public static StreamTombstonedException FromRpcStatus(Google.Rpc.Status ex) { + var details = ex.GetDetail(); + return new StreamTombstonedException(details.Stream); + } +} diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs index ce2d05c70..05ee4398e 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs @@ -80,10 +80,9 @@ await session.RequestStream return new MultiStreamAppendResponse(response.Position, responses); } catch (RpcException ex) { var status = ex.GetRpcStatus()!; - throw status.GetDetail() switch { { Reason: "STREAM_REVISION_CONFLICT" } => WrongExpectedVersionException.FromRpcException(ex), - { Reason: "STREAM_TOMBSTONED" } => StreamDeletedException.FromRpcException(ex), + { Reason: "STREAM_TOMBSTONED" } => StreamTombstonedException.FromRpcException(ex), { Reason: "APPEND_RECORD_SIZE_EXCEEDED" } => AppendRecordSizeExceededException.FromRpcException(ex), { Reason: "APPEND_TRANSACTION_SIZE_EXCEEDED" } => AppendTransactionMaxSizeExceededException.FromRpcException(ex), _ => ex diff --git a/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs b/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs index 401efcf20..3bac3c77a 100644 --- a/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs +++ b/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs @@ -23,8 +23,7 @@ static GlobalEnvironment() { static void EnsureDefaults(IConfiguration configuration) { // internal defaults - // configuration.EnsureValue("TESTCONTAINER_KURRENTDB_IMAGE", "docker.cloudsmith.io/eventstore/kurrent-staging/kurrentdb:ci"); - configuration.EnsureValue("TESTCONTAINER_KURRENTDB_IMAGE", "docker.cloudsmith.io/eventstore/kurrent-preview/kurrentdb:25.1.0-rc.1-x64-8.0-jammy"); + configuration.EnsureValue("TESTCONTAINER_KURRENTDB_IMAGE", "docker pull docker.cloudsmith.io/eventstore/kurrent-latest/kurrentdb:latest"); // database defaults configuration.EnsureValue("EVENTSTORE_TELEMETRY_OPTOUT", "true"); diff --git a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs index f4b1eb4d9..de4caf7f9 100644 --- a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs +++ b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs @@ -139,7 +139,7 @@ public async Task appending_events_throws_deleted_exception_when_tombstoned() { var appendTask = async () => await Fixture.Streams.MultiStreamAppendAsync(requests); // Assert - var rex = await appendTask.ShouldThrowAsync(); + var rex = await appendTask.ShouldThrowAsync(); rex.Stream.ShouldBe(stream); } } From f3c0830164d7609053f8f1dc96d3751ae24257ab Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 21 Oct 2025 20:19:29 +0400 Subject: [PATCH 27/29] Update metadata handling to use string dictionary --- .../Diagnostics/EventMetadataExtensions.cs | 2 +- .../Core/Common/MetadataExtensions.cs | 64 +++++++++++ src/KurrentDB.Client/Core/ValueMapper.cs | 31 +----- .../Streams/Model/Metadata/MetadataDecoder.cs | 105 ------------------ .../Streams/Model/StreamsClientMapper.cs | 12 +- .../GlobalEnvironment.cs | 2 +- .../Streams/MultiStreamAppendTests.cs | 61 ++-------- 7 files changed, 85 insertions(+), 192 deletions(-) delete mode 100644 src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs diff --git a/src/KurrentDB.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs b/src/KurrentDB.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs index d259c6e90..f1e0b5edb 100644 --- a/src/KurrentDB.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs +++ b/src/KurrentDB.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs @@ -7,7 +7,7 @@ namespace KurrentDB.Client.Diagnostics; static class EventMetadataExtensions { - public static void InjectTracingContext(this Dictionary metadata, Activity? activity) { + public static void InjectTracingContext(this Dictionary metadata, Activity? activity) { if (!KurrentDBClientDiagnostics.ActivitySource.HasListeners() || activity is null) return; metadata[TracingConstants.Metadata.TraceId] = activity.TraceId.ToString(); diff --git a/src/KurrentDB.Client/Core/Common/MetadataExtensions.cs b/src/KurrentDB.Client/Core/Common/MetadataExtensions.cs index c2136f138..72edd0bd0 100644 --- a/src/KurrentDB.Client/Core/Common/MetadataExtensions.cs +++ b/src/KurrentDB.Client/Core/Common/MetadataExtensions.cs @@ -1,8 +1,60 @@ +using System.Text.Json; using Grpc.Core; namespace KurrentDB.Client; static class MetadataExtensions { + /// + /// Encodes a dictionary of string key-value pairs as JSON metadata bytes. + /// + /// The dictionary to encode. + /// UTF-8 encoded JSON bytes representing the metadata. + public static ReadOnlyMemory Encode(this Dictionary metadata) => + JsonSerializer.SerializeToUtf8Bytes(metadata); + + /// + /// Encodes an anonymous object as JSON metadata bytes. + /// + /// The object to encode. + /// UTF-8 encoded JSON bytes representing the metadata. + public static ReadOnlyMemory Encode(this object metadata) => + JsonSerializer.SerializeToUtf8Bytes(metadata); + + /// + /// Decodes JSON metadata bytes as a dictionary of string key-value pairs. + /// + /// The metadata bytes to decode. + /// A dictionary of string key-value pairs, or null if the metadata is empty. + public static Dictionary? Decode(this ReadOnlyMemory metadata) { + if (metadata.IsEmpty) + return null; + + return JsonSerializer.Deserialize>(metadata.Span); + } + + /// + /// Decodes the metadata from an event record as a dictionary of string key-value pairs. + /// + /// The event record containing metadata to decode. + /// A dictionary of string key-value pairs, or null if the metadata is empty. + public static Dictionary? Decode(this EventRecord eventRecord) => + eventRecord.Metadata.Decode(); + + /// + /// Decodes the metadata from a resolved event as a dictionary of string key-value pairs. + /// + /// The resolved event containing metadata to decode. + /// A dictionary of string key-value pairs, or null if the metadata is empty. + public static Dictionary? Decode(this ResolvedEvent resolvedEvent) => + resolvedEvent.OriginalEvent.Metadata.Decode(); + + /// + /// Attempts to retrieve a value from the gRPC metadata by key. + /// + /// The gRPC metadata collection. + /// The key to search for. + /// When this method returns, contains the value associated with the key, if found; otherwise, null. + /// true if the key was found; otherwise, false. public static bool TryGetValue(this Metadata metadata, string key, out string? value) { value = default; @@ -17,11 +69,23 @@ public static bool TryGetValue(this Metadata metadata, string key, out string? v return false; } + /// + /// Retrieves a stream state value from the gRPC metadata by key. + /// + /// The gRPC metadata collection. + /// The key to search for. + /// The parsed stream state value if found and valid; otherwise, . public static StreamState GetStreamState(this Metadata metadata, string key) => metadata.TryGetValue(key, out var s) && ulong.TryParse(s, out var value) ? value : StreamState.NoStream; + /// + /// Retrieves an integer value from the gRPC metadata by key. + /// + /// The gRPC metadata collection. + /// The key to search for. + /// The parsed integer value if found and valid; otherwise, 0. public static int GetIntValueOrDefault(this Metadata metadata, string key) => metadata.TryGetValue(key, out var s) && int.TryParse(s, out var value) ? value diff --git a/src/KurrentDB.Client/Core/ValueMapper.cs b/src/KurrentDB.Client/Core/ValueMapper.cs index 82ed1280f..aad52fa69 100644 --- a/src/KurrentDB.Client/Core/ValueMapper.cs +++ b/src/KurrentDB.Client/Core/ValueMapper.cs @@ -6,39 +6,12 @@ namespace KurrentDB.Client; [PublicAPI] static class ValueMapper { - public static MapField MapToMapValue(this Dictionary source) => + public static MapField MapToMapValue(this Dictionary source) => source.Aggregate( new MapField(), (seed, entry) => { - seed.Add(entry.Key, MapToValue(entry.Value)); + seed.Add(entry.Key, new Value { StringValue = entry.Value }); return seed; } ); - - public static Value MapToValue(this object? source) { - return source switch { - null => new() { NullValue = NullValue.NullValue }, - string x => new() { StringValue = x }, - bool x => new() { BoolValue = x }, - int x => new() { NumberValue = x }, - long x => new() { NumberValue = x }, - float x => new() { NumberValue = x }, - double x => new() { NumberValue = x }, - - DateTime x => new() { StringValue = x.ToUniversalTime().ToString("O") }, - DateTimeOffset x => new() { StringValue = x.ToUniversalTime().ToString("O") }, - TimeSpan x => new() { StringValue = x.ToString() }, - - byte[] x => new() { StringValue = Convert.ToBase64String(x) }, - ReadOnlyMemory x => new() { -#if NET48 - StringValue = Convert.ToBase64String(x.ToArray()) -#else - StringValue = Convert.ToBase64String(x.Span) -#endif - }, - - _ => new() { StringValue = source.ToString() } // any other type is converted to string - }; - } } diff --git a/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs b/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs deleted file mode 100644 index cca9c3b26..000000000 --- a/src/KurrentDB.Client/Streams/Streams/Model/Metadata/MetadataDecoder.cs +++ /dev/null @@ -1,105 +0,0 @@ -// ReSharper disable InconsistentNaming - -using System.Buffers; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; -using JetBrains.Annotations; -using System.Text; -using KurrentDB.Client.Core.Internal.Exceptions; -using static KurrentDB.Client.Constants; -using Enum = System.Enum; -using Type = System.Type; - -namespace KurrentDB.Client; - -/// -/// Provides methods to decode metadata. -/// -[PublicAPI] -public static class MetadataDecoder { - public static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerOptions.Default) { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - PropertyNameCaseInsensitive = false, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, - UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, - Converters = { - new MetadataJsonConverter(), - new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) - } - }; - - /// Decodes the provided metadata in a read-only byte memory structure into a dictionary - /// containing key-value pairs of metadata properties. - /// The read-only memory of bytes containing the metadata to decode. - /// A dictionary representing the decoded metadata, where keys are strings and values are objects, - /// or null if the decoding process fails. - /// Thrown when the metadata is not valid JSON or contains unsupported property values. - public static Dictionary Decode(ReadOnlyMemory metadata) { - try { - return JsonSerializer.Deserialize>(metadata.Span, JsonSerializerOptions) ?? throw new InvalidOperationException(); - } catch (Exception ex) { - throw new ArgumentException( - $"Event metadata must be valid JSON with property values limited to: null, boolean, number, string, Guid, DateTime, TimeSpan, or Base64-encoded byte arrays. " + - $"Complex objects and arrays are not supported. This limitation will be removed in the next major release. " + - $"Deserialization failed: {ex.Message}", - ex - ); - } - } -} - -public class MetadataJsonConverter : JsonConverter> { - public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var metadata = new Dictionary(); - - while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { - if (reader.GetString() is not { } propertyName || string.IsNullOrWhiteSpace(propertyName)) - throw new JsonException("Property name cannot be empty or whitespace"); - - reader.Read(); - - var value = reader.TokenType switch { - JsonTokenType.None => null, - JsonTokenType.Null => null, - JsonTokenType.True => true, - JsonTokenType.False => false, - JsonTokenType.String => ParseString(reader, propertyName), - JsonTokenType.Number => reader.GetDouble(), - _ => throw new JsonException($"Unsupported metadata value type ({reader.TokenType}) for property '{propertyName}'") - }; - - metadata[propertyName] = value; - } - - return metadata; - - static object? ParseString(Utf8JsonReader reader, string propertyName) { - var value = reader.GetString(); - - if (propertyName.Equals(Metadata.SchemaName, StringComparison.OrdinalIgnoreCase)) - return string.IsNullOrWhiteSpace(value) ? "" : value; - - if (propertyName.Equals(Metadata.SchemaFormat, StringComparison.OrdinalIgnoreCase)) - return Enum.TryParse(value, ignoreCase: true, out var format) - ? format - : SchemaDataFormat.Unspecified; - - if (reader.TryGetGuid(out var guid)) return guid; - if (reader.TryGetDateTime(out var dateTime)) return dateTime; - if (reader.TryGetTimeSpan(out var timeSpan)) return timeSpan; - if (reader.TryGetBytesFromBase64(out var bytes)) return new ReadOnlyMemory(bytes); - - if (DateTime.TryParse(value, null, DateTimeStyles.RoundtripKind, out var isoDateTime)) - return isoDateTime; - - return value; - } - } - - public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) => - JsonSerializer.Serialize(writer, value, options); -} diff --git a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs index ea8a44d92..d8185f062 100644 --- a/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs +++ b/src/KurrentDB.Client/Streams/Streams/Model/StreamsClientMapper.cs @@ -16,10 +16,16 @@ public static async IAsyncEnumerable Map(this IEnumerable Map(this EventData source) { - Dictionary metadata = new(); + Dictionary metadata; - if (!source.Metadata.IsEmpty) - metadata = MetadataDecoder.Decode(source.Metadata); + try { + metadata = source.Metadata.Decode() ?? new Dictionary(); + } catch (Exception ex) { + throw new ArgumentException( + "Failed to decode event metadata. The metadata may be missing, malformed, or not in valid JSON format. Please verify the event's metadata structure.", + ex + ); + } metadata.InjectTracingContext(Activity.Current); diff --git a/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs b/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs index 3bac3c77a..2ef7b6e5e 100644 --- a/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs +++ b/test/KurrentDB.Client.Tests.Common/GlobalEnvironment.cs @@ -23,7 +23,7 @@ static GlobalEnvironment() { static void EnsureDefaults(IConfiguration configuration) { // internal defaults - configuration.EnsureValue("TESTCONTAINER_KURRENTDB_IMAGE", "docker pull docker.cloudsmith.io/eventstore/kurrent-latest/kurrentdb:latest"); + configuration.EnsureValue("TESTCONTAINER_KURRENTDB_IMAGE", "docker.cloudsmith.io/eventstore/kurrent-latest/kurrentdb:latest"); // database defaults configuration.EnsureValue("EVENTSTORE_TELEMETRY_OPTOUT", "true"); diff --git a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs index de4caf7f9..45bd38458 100644 --- a/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs +++ b/test/KurrentDB.Client.Tests/Streams/MultiStreamAppendTests.cs @@ -1,6 +1,3 @@ -using System.Text.Json; -using Humanizer; - namespace KurrentDB.Client.Tests.Streams; [Trait("Category", "Target:Streams")] @@ -22,7 +19,7 @@ public async Task append_events_with_invalid_metadata_format_throws_exceptions() .MultiStreamAppendAsync(requests).AsTask() .ShouldThrowAsync(); - Assert.Contains("Deserialization failed:", exception.Message); + exception.Message.ShouldContain("Failed to decode event metadata"); } [MinimumVersion.Fact(25, 1)] @@ -31,23 +28,13 @@ public async Task append_events_to_multiple_streams() { var stream1 = Fixture.GetStreamName(); var stream2 = Fixture.GetStreamName(); - var expectedMetadata = new TestMetadata { - StringValue = "Foo", - IntegerValue = 0, - BooleanValue = true, - DoubleValue = 2.718281828, - DateTimeValue = DateTime.UtcNow, - TimeSpanValue = 2.5.Hours(), - NullTimeSpanValue = null, - ZeroTimeSpanValue = TimeSpan.Zero, - ByteArrayValue = "Bar"u8.ToArray() + var expectedMetadata = new Dictionary { + ["Name"] = Fixture.Faker.Person.FullName }; - var metadataBytes = JsonSerializer.SerializeToUtf8Bytes(expectedMetadata); - AppendStreamRequest[] requests = [ - new(stream1, StreamState.NoStream, Fixture.CreateTestEvents(3, metadata: metadataBytes).ToArray()), - new(stream2, StreamState.NoStream, Fixture.CreateTestEvents(2, metadata: metadataBytes).ToArray()) + new(stream1, StreamState.NoStream, Fixture.CreateTestEvents(3, metadata: expectedMetadata.Encode()).ToArray()), + new(stream2, StreamState.NoStream, Fixture.CreateTestEvents(2, metadata: expectedMetadata.Encode()).ToArray()) ]; // Act @@ -71,35 +58,15 @@ public async Task append_events_to_multiple_streams() { .ReadStreamAsync(Direction.Forwards, stream2, StreamPosition.Start, 10) .ToArrayAsync(); - var metadata = MetadataDecoder.Decode(stream1Events.First().OriginalEvent.Metadata); + var metadata = stream1Events.First().Decode(); stream1Events.Length.ShouldBe(3); stream2Events.Length.ShouldBe(2); metadata.ShouldNotBeNull(); metadata[Constants.Metadata.SchemaName].ShouldBe("test-event-type"); - metadata[Constants.Metadata.SchemaFormat].ShouldBe(SchemaDataFormat.Json); - metadata["StringValue"].ShouldBe(expectedMetadata.StringValue); - metadata["BooleanValue"].ShouldBe(expectedMetadata.BooleanValue); - - metadata["IntegerValue"].ShouldBe(expectedMetadata.IntegerValue); - metadata["DoubleValue"].ShouldBe(expectedMetadata.DoubleValue); - - metadata["DateTimeValue"].ShouldBe(expectedMetadata.DateTimeValue); - metadata["TimeSpanValue"].ShouldBe(expectedMetadata.TimeSpanValue); - metadata["NullTimeSpanValue"].ShouldBeNull(); - metadata["ZeroTimeSpanValue"].ShouldBe(expectedMetadata.ZeroTimeSpanValue); - metadata["ByteArrayValue"].ShouldBe(expectedMetadata.ByteArrayValue); - - metadata["BooleanValue"]?.GetType().ShouldBe(typeof(bool)); - metadata["StringValue"]?.GetType().ShouldBe(typeof(string)); - metadata["IntegerValue"]?.GetType().ShouldBe(typeof(double)); - metadata["DoubleValue"]?.GetType().ShouldBe(typeof(double)); - metadata["DateTimeValue"]?.GetType().ShouldBe(typeof(DateTime)); - metadata["TimeSpanValue"]?.GetType().ShouldBe(typeof(TimeSpan)); - metadata["NullTimeSpanValue"]?.GetType().ShouldBe(typeof(TimeSpan)); - metadata["ZeroTimeSpanValue"]?.GetType().ShouldBe(typeof(TimeSpan)); - metadata["ByteArrayValue"]?.GetType().ShouldBe(typeof(ReadOnlyMemory)); + metadata[Constants.Metadata.SchemaFormat].ShouldBe(nameof(SchemaDataFormat.Json)); + metadata["Name"].ShouldBe(expectedMetadata["Name"]); } [MinimumVersion.Fact(25, 1)] @@ -143,15 +110,3 @@ public async Task appending_events_throws_deleted_exception_when_tombstoned() { rex.Stream.ShouldBe(stream); } } - -public class TestMetadata { - public string? StringValue { get; init; } - public int? IntegerValue { get; init; } - public bool? BooleanValue { get; init; } - public double? DoubleValue { get; init; } - public DateTime? DateTimeValue { get; init; } - public TimeSpan? TimeSpanValue { get; init; } - public TimeSpan? NullTimeSpanValue { get; init; } - public TimeSpan? ZeroTimeSpanValue { get; init; } - public byte[]? ByteArrayValue { get; init; } -} From a22c7dfb8975ba50fb9abb0c99e7138a1b7b4254 Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 22 Oct 2025 11:16:23 +0400 Subject: [PATCH 28/29] docs --- docs/api/appending-events.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/api/appending-events.md b/docs/api/appending-events.md index e713d36b9..26ac82986 100644 --- a/docs/api/appending-events.md +++ b/docs/api/appending-events.md @@ -172,13 +172,24 @@ await client.MultiStreamAppendAsync(requests); The result returns the position of the last appended record in the transaction and a collection of responses for each stream appended in the transaction. ::: warning -If you are storing metadata, it must currently be a valid JSON that can be deserialized into a -`Dictionary`. This means any metadata you attach to an event should be structured as a JSON object, not as a primitive value or array. +The metadata for an event must be a valid JSON object where both keys and values are strings. It is essential that the JSON is well-formed and not missing, as any malformed or absent metadata will result in an `ArgumentException` being thrown. + +You can use the provided `Encode` and `Decode` extension methods when writing +and reading metadata. For example: -When reading those events you can use the metadata decoder utility class to decode your metadata: ```cs -var dictionary = MetadataDecoder.Decode(metadataBytes); +var metadata = new Dictionary +{ + { "userId", "user-456" } +}; + +// encode to bytes before appending +var metadataBytes = metadata.Encode(); ``` -This requirement ensures compatibility with KurrentDB's current metadata handling, and the restriction will be lifted in the next major release. +And when reading metadata back: + +```cs +var metadata = metadataBytes.Decode(); +``` ::: From 3489858b565b4bb0d7fa9d1bd97f37f4d5fa18ad Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 22 Oct 2025 17:15:37 +0400 Subject: [PATCH 29/29] Remove dead code and improve docs --- .../Core/Common/ValueExtensions.cs | 274 ------------------ src/KurrentDB.Client/Core/ValueMapper.cs | 2 +- .../Streams/KurrentDBClient.MultiAppend.cs | 2 +- 3 files changed, 2 insertions(+), 276 deletions(-) delete mode 100644 src/KurrentDB.Client/Core/Common/ValueExtensions.cs diff --git a/src/KurrentDB.Client/Core/Common/ValueExtensions.cs b/src/KurrentDB.Client/Core/Common/ValueExtensions.cs deleted file mode 100644 index f03bfdaa4..000000000 --- a/src/KurrentDB.Client/Core/Common/ValueExtensions.cs +++ /dev/null @@ -1,274 +0,0 @@ -using System.Globalization; -using System.Runtime.CompilerServices; -using Google.Protobuf.WellKnownTypes; -using Enum = System.Enum; - -namespace KurrentDB.Client; - -public static class ValueExtensions { - // Epsilon tolerance for floating point comparisons to handle precision issues - const double IntegerEpsilon = 1e-10; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryGetString(this Value value, out string? result) { - if (value.KindCase == Value.KindOneofCase.StringValue) { - result = value.StringValue; - return true; - } - - result = null; - return false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryGetBoolean(this Value value, out bool result) { - switch (value.KindCase) { - case Value.KindOneofCase.BoolValue: - result = value.BoolValue; - return true; - case Value.KindOneofCase.StringValue when bool.TryParse(value.StringValue, out result): - return true; - default: - result = false; - return false; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryGetDouble(this Value value, out double result) { - switch (value.KindCase) { - case Value.KindOneofCase.NumberValue: - result = value.NumberValue; - return true; - case Value.KindOneofCase.StringValue when double.TryParse(value.StringValue, out result): - return true; - default: - result = 0; - return false; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryGetInt32(this Value value, out int result) { - switch (value.KindCase) { - case Value.KindOneofCase.NumberValue: { - var d = value.NumberValue; - if (d is >= int.MinValue and <= int.MaxValue) { - var truncated = Math.Truncate(d); - if (Math.Abs(d - truncated) < IntegerEpsilon) { - result = (int)truncated; - return true; - } - } - - result = 0; - return false; - } - case Value.KindOneofCase.StringValue when int.TryParse(value.StringValue, out result): - return true; - default: - result = 0; - return false; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryGetInt64(this Value value, out long result) { - switch (value.KindCase) { - case Value.KindOneofCase.NumberValue: { - var d = value.NumberValue; - if (d is >= long.MinValue and <= long.MaxValue) { - var truncated = Math.Truncate(d); - if (Math.Abs(d - truncated) < IntegerEpsilon) { - result = (long)truncated; - return true; - } - } - - result = 0; - return false; - } - case Value.KindOneofCase.StringValue when long.TryParse(value.StringValue, out result): - return true; - default: - result = 0; - return false; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryGetSingle(this Value value, out float result) { - switch (value.KindCase) { - case Value.KindOneofCase.NumberValue: { - var d = value.NumberValue; - if (d is >= float.MinValue and <= float.MaxValue) { - result = (float)d; - return true; - } - - result = 0; - return false; - } - case Value.KindOneofCase.StringValue when float.TryParse(value.StringValue, out result): - return true; - default: - result = 0; - return false; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryGetDecimal(this Value value, out decimal result) { - switch (value.KindCase) { - case Value.KindOneofCase.NumberValue: { - var d = value.NumberValue; - if (d is >= (double)decimal.MinValue and <= (double)decimal.MaxValue) { - result = (decimal)d; - return true; - } - - result = 0; - return false; - } - case Value.KindOneofCase.StringValue when decimal.TryParse(value.StringValue, out result): - return true; - default: - result = 0; - return false; - } - } - - public static bool TryGetGuid(this Value value, out Guid result) { - if (value.KindCase == Value.KindOneofCase.StringValue) - return Guid.TryParse(value.StringValue, out result); - - result = Guid.Empty; - return false; - } - - public static bool TryGetDateTime(this Value value, out DateTime result) { - if (value.KindCase == Value.KindOneofCase.StringValue) - return DateTime.TryParse(value.StringValue, out result); - - result = default; - return false; - } - - public static bool TryGetDateTimeOffset(this Value value, out DateTimeOffset result) { - if (value.KindCase == Value.KindOneofCase.StringValue) - return DateTimeOffset.TryParse(value.StringValue, out result); - - result = default; - return false; - } - - public static bool TryGetTimeSpan(this Value value, out TimeSpan result) { - if (value.KindCase == Value.KindOneofCase.StringValue) { - // Try standard TimeSpan parsing first - if (TimeSpan.TryParse(value.StringValue, out result)) - return true; - - // Try ISO 8601 duration format (PT1H30M, P1DT2H, etc.) - if (value.StringValue.Length > 1 && value.StringValue[0] == 'P') { - try { - result = System.Xml.XmlConvert.ToTimeSpan(value.StringValue); - return true; - } catch { - // Fall through to false - } - } - } - - result = TimeSpan.Zero; - return false; - } - - public static bool TryGetEnum(this Value value, out T result) where T : struct, Enum { - if (value.KindCase == Value.KindOneofCase.StringValue) - return Enum.TryParse(value.StringValue, ignoreCase: true, out result); - - result = default; - return false; - } - - static string GetDisplayValue(Value value) => value.KindCase switch { - Value.KindOneofCase.StringValue => $"'{value.StringValue}'", - Value.KindOneofCase.NumberValue => value.NumberValue.ToString(CultureInfo.InvariantCulture), - Value.KindOneofCase.BoolValue => value.BoolValue.ToString(), - Value.KindOneofCase.NullValue => "null", - Value.KindOneofCase.None => "none", - _ => $"<{value.KindCase}>" - }; - - public static string GetString(this Value value) { - if (TryGetString(value, out var result) && result != null) - return result; - throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to String"); - } - - public static bool GetBoolean(this Value value) { - return TryGetBoolean(value, out var result) - ? result - : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to Boolean"); - } - - public static double GetDouble(this Value value) { - return TryGetDouble(value, out var result) - ? result - : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to Double"); - } - - public static int GetInt32(this Value value) { - return TryGetInt32(value, out var result) - ? result - : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to Int32"); - } - - public static long GetInt64(this Value value) { - return TryGetInt64(value, out var result) - ? result - : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to Int64"); - } - - public static float GetSingle(this Value value) { - return TryGetSingle(value, out var result) - ? result - : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to Single"); - } - - public static decimal GetDecimal(this Value value) { - return TryGetDecimal(value, out var result) - ? result - : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to Decimal"); - } - - public static Guid GetGuid(this Value value) { - return TryGetGuid(value, out var result) - ? result - : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to Guid"); - } - - public static DateTime GetDateTime(this Value value) { - return TryGetDateTime(value, out var result) - ? result - : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to DateTime"); - } - - public static DateTimeOffset GetDateTimeOffset(this Value value) { - return TryGetDateTimeOffset(value, out var result) - ? result - : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to DateTimeOffset"); - } - - public static TimeSpan GetTimeSpan(this Value value) { - return TryGetTimeSpan(value, out var result) - ? result - : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to TimeSpan"); - } - - public static T GetEnum(this Value value) where T : struct, Enum { - return TryGetEnum(value, out var result) - ? result - : throw new InvalidOperationException($"Cannot convert Value of type {value.KindCase} with value {GetDisplayValue(value)} to {typeof(T).Name}"); - } -} diff --git a/src/KurrentDB.Client/Core/ValueMapper.cs b/src/KurrentDB.Client/Core/ValueMapper.cs index aad52fa69..f8b6e1caa 100644 --- a/src/KurrentDB.Client/Core/ValueMapper.cs +++ b/src/KurrentDB.Client/Core/ValueMapper.cs @@ -10,7 +10,7 @@ public static MapField MapToMapValue(this Dictionary(), (seed, entry) => { - seed.Add(entry.Key, new Value { StringValue = entry.Value }); + seed.Add(entry.Key, Value.ForString(entry.Value)); return seed; } ); diff --git a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs index 05ee4398e..ca2c1cb64 100644 --- a/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs +++ b/src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs @@ -27,7 +27,7 @@ public partial class KurrentDBClient { /// A task that represents the asynchronous operation, with a result of type , indicating the outcome of the operation. /// /// On success, returns containing the successful append results. - /// , , , or . + /// , , , ., or . /// /// /// Thrown if the server does not support multi-stream append functionality (requires server version 25.1 or higher).

L5}!wbyJzinby~Y}wl*>_uq3k6=ZVq4m$0sq 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 -1) { + var c = document.querySelector("link[rel='canonical']"); + args.push({ + __t: "bpc", + c: c && c.getAttribute("href") || undefined, + p: location.pathname, + u: location.href, + s: location.search, + t: document.title, + r: document.referrer + }); + } + + args.unshift(e); + analytics.push(args); + return analytics; + }; + }; + + for (var i = 0; i < analytics.methods.length; i++) { + var key = analytics.methods[i]; + analytics[key] = analytics.factory(key); + } + + analytics.load = function (key, options) { + var t = document.createElement("script"); + t.type = "text/javascript"; + t.async = true; + t.setAttribute("data-global-segment-analytics-key", globalAnalyticsKey) + t.src = "https://cloud.kurrent.io/segment/ajs/REDACTED"; + + var first = document.getElementsByTagName("script")[0]; + first.parentNode.insertBefore(t, first); + analytics._loadOptions = options; + }; + analytics._writeKey = "REDACTED"; + analytics.SNIPPET_VERSION = "5.2.1"; + analytics.load("REDACTED"); +})(); diff --git a/docs/.vuepress/public/logo-white.png b/docs/.vuepress/public/logo-white.png new file mode 100644 index 0000000000000000000000000000000000000000..107eb88ce759d9aaf307148b7dcc4bed9423269c GIT binary patch literal 15352 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 literal 0 HcmV?d00001 diff --git a/docs/.vuepress/public/robots.txt b/docs/.vuepress/public/robots.txt new file mode 100644 index 000000000..dbe84f0a1 --- /dev/null +++ b/docs/.vuepress/public/robots.txt @@ -0,0 +1,15 @@ +User-agent: * + +Disallow: /.algolia/ +Disallow: /.github/ +Disallow: /.vscode/ +Disallow: /import/ +Disallow: /.gitattributes +Disallow: /.gitignore +Disallow: /.node-version +Disallow: /codeql-analysis.yml +Disallow: /package.json +Disallow: /serve.js +Disallow: /yarn.lock + +Sitemap: https://docs.kurrent.io/sitemap.xml diff --git a/docs/.vuepress/public/video.png b/docs/.vuepress/public/video.png new file mode 100644 index 0000000000000000000000000000000000000000..b6fcd2c1d9fc2a3858892cd7213adcf2bc90445e GIT binary patch literal 8640 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 p, +.theme-hope-content:not(.custom) > ol p, +.theme-hope-content:not(.custom) > ul p { + text-align: left; +} + +html[data-theme="dark"] #kapa-widget-portal { + code { + background: rgba(127, 127, 127, 0.12); + } + + blockquote { + border-color: #eee; + } +} + +/* Fix for center-alignment issue on `.not-found` pages */ +.vp-page.not-found .theme-hope-content, +.vp-page.not-found .vp-breadcrumb, +.vp-page.not-found .vp-page-title, +.vp-page.not-found .vp-toc-placeholder { + text-align: initial; + width: var(--content-width, 740px); +} + +.vp-hero-info { + text-align: center; +} + +.DocSearch-Button-Keys { + display: none; +} + +.DocSearch-Button-Placeholder { + padding-right: 30px; +} + +#main-description { + max-width: 50rem; +} + +.vp-highlight-header { + text-align: center; +} + +.vp-highlight-info-wrapper:only-child { + flex: 1; +} + +/*Remove deprecation notice*/ +div[style*="z-index: 2147483647"] { + display: none !important; + visibility: hidden !important; + opacity: 0 !important; + position: absolute !important; + pointer-events: none !important; +} + +.vp-hero-title { + margin: 0.5rem 0; + background: linear-gradient( 120deg, var(--vp-c-accent-hover), var(--vp-c-accent) 30%, var(--end-gradient) 120% ); + -webkit-background-clip: text; + background-clip: text; + font-weight: bold; + font-size: 3.6rem; + font-family: var(--vp-font); + line-height: 1.5; + -webkit-text-fill-color: transparent; +} + +.vp-highlight-item { + list-style: none; +} + +@media screen and (max-width: 480px) { + .vp-action, + .custom-class-kapa, + .kapa-container { + display: none; + } +} + +.vp-highlight-title { + font-size: 1.4rem; +} diff --git a/docs/.vuepress/styles/palette.scss b/docs/.vuepress/styles/palette.scss new file mode 100644 index 000000000..f87d09207 --- /dev/null +++ b/docs/.vuepress/styles/palette.scss @@ -0,0 +1,193 @@ +@use 'sass:color'; + +/* Primary colors */ +$main-color: #631b3a; +$secondary-color: #B75781; + +/* Light Theme Colors */ +$color-background-light: #ffffff; +$color-foreground-light: #000000; +$color-link-light: #479bff; +$color-title-light: $main-color; + +/* Dark Theme Colors */ +$color-background-dark: #080b0d; +$color-foreground-dark: #f0f0f0; +$color-title-dark: #f0f0f0; + +$theme-color-light: color.mix(black, $main-color, 10%); // Darken by 10% +$theme-color-soft: #{color.scale(#631b3a, $lightness: 85%)}; +$theme-color-dark: color.mix(white, $main-color, 10%); // Lighten by 10% +$theme-color-mask: color.adjust( $secondary-color, $alpha: -0.85 ); // Reduce opacity + +/* CSS Variable Definitions */ +:root { + --main-color: #{$main-color}; + --secondary-color: #{$secondary-color}; + --end-gradient: #{$secondary-color}; + --color-link: #{$color-link-light}; + --theme-name: light; + --theme-shade: light; + --theme-contrast: low; + --color-background: #{$color-background-light}; + --color-foreground: #{$color-foreground-light}; + --color-title: var(--main-color); + --color-title-contrast: #ffffff; + --color-highlight: var(--main-color); + --color-highlight-contrast: #ffffff; + --color-focus: #8d5fff; + --color-focus-contrast: #ffffff; + --color-error: #b75781; + --color-error-contrast: #ffffff; + --color-error-alt: #dbabc0; + --color-error-alt-contrast: #000000; + --color-warning: #7d6f05; + --color-warning-contrast: #ffffff; + --color-warning-alt: #fcf29b; + --color-warning-alt-contrast: #000000; + --color-success: #2c667d; + --color-success-contrast: #ffffff; + --color-success-alt: #dbf9ee; + --color-success-alt-contrast: #000000; + --color-info: #4855a7; + --color-info-contrast: #ffffff; + --color-info-alt: #edf1fc; + --color-info-alt-contrast: #000000; + --color-shade-10: #f5f8f9; + --color-shade-20: #ecf1f3; + --color-shade-30: #e3eaed; + --color-shade-40: #d9e3e8; + --color-shade-50: #d0dce2; + --color-shade-60: #b4c8d1; + --color-overlay: #a79ab8; + --color-overlay-alpha: 0.75; + --color-disabled: #ecf1f3; + --color-disabled-border: #ecf1f3; + --color-disabled-contrast: #000000; + --code-fg: #383a42; + --code-bg: #fffffe; + --code-literal: #0184bc; + --code-symbol: #4078f2; + --code-keyword: #a626a4; + --code-string: #50a14f; + --code-error: #e45649; + --code-variable: #986801; + --code-class: #c18401; + --code-comment: #6f7f90; +} + +/* Dark Theme */ +@media (prefers-color-scheme: dark) { + :root { + --theme-name: dark; + --theme-shade: dark; + --theme-contrast: low; + --end-gradient: #{$main-color}; + --color-background: #{$color-background-dark}; + --color-foreground: #{$color-foreground-dark}; + --theme-name: dark; + --theme-shade: dark; + --theme-contrast: low; + --color-background: #080b0d; + --color-foreground: #f0f0f0; + --color-title: #f0f0f0; + --color-title-contrast: #080b0d; + --color-highlight: #64edbb; + --color-highlight-contrast: #080b0d; + --color-link: #479bff; + --color-link-contrast: #080b0d; + --color-focus: #8d5fff; + --color-focus-contrast: #080b0d; + --color-error: #ff4a80; + --color-error-contrast: #080b0d; + --color-error-alt: #631b3a; + --color-error-alt-contrast: #ffffff; + --color-warning: #ffed4e; + --color-warning-contrast: #080b0d; + --color-warning-alt: #7d6f05; + --color-warning-alt-contrast: #ffffff; + --color-success: #64edbb; + --color-success-contrast: #080b0d; + --color-success-alt: #2c667d; + --color-success-alt-contrast: #ffffff; + --color-info: #479bff; + --color-info-contrast: #080b0d; + --color-info-alt: #4855a7; + --color-info-alt-contrast: #ffffff; + --color-shade-10: #161c1e; + --color-shade-20: #1e2528; + --color-shade-30: #262e31; + --color-shade-40: #2e383b; + --color-shade-50: #364146; + --color-shade-60: #3f4c50; + --color-overlay: #2c667d; + --color-overlay-alpha: 0.75; + --color-disabled: #1e2528; + --color-disabled-border: #1e2528; + --color-disabled-contrast: #f0f0f0; + --code-fg: #d7dae0; + --code-bg: #313440; + --code-literal: #e5c07b; + --code-symbol: #56b6c2; + --code-keyword: #c678dd; + --code-string: #98c379; + --code-error: #e05252; + --code-variable: #e06c75; + --code-class: #e5c07b; + --code-comment: #5c6370; + } +} + +$vp-font: 'Solina, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", STHeiti, "Microsoft YaHei", SimSun, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; +$vp-font-heading: 'Solina, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", STHeiti, "Microsoft YaHei", SimSun, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; + +html[data-theme="dark"] { + --theme-color: var(--secondary-color); + --vp-c-accent-hover: var(--secondary-color); + --vp-c-accent: var(--secondary-color); + --vp-c-accent-bg: var(--secondary-color); + --vp-c-accent-soft: var(--main-color); + --theme-color-light: #{$theme-color-light}; + --theme-color-dark: #{$theme-color-dark}; + --theme-color-mask: #{$theme-color-mask}; + --end-gradient: #{$main-color}; +} + +html[data-theme="light"] { + --theme-color: var(--main-color); + --vp-c-accent-hover: var(--main-color); + --vp-c-accent: var(--main-color); + --vp-c-accent-bg: var(--main-color); + --vp-c-accent-soft: #{$theme-color-soft}; + --theme-color-light: #{$theme-color-light}; + --theme-color-dark: #{$theme-color-dark}; + --theme-color-mask: #{$theme-color-mask}; +} + +$vp-c-bg: ( light: #fff, dark: #080b0d, ); + +$vp-c-bg-elv-soft: ( light: #fff, dark: #080b0d, ); + +$font-body: 15px; + +/* Override Code Block Background */ +$sidebar-width: 18rem; + +$vp-c-text: ( light: #000, dark: #f0f0f0, ); + +.hint-container.important a, +.hint-container.info a, +.hint-container.note a, +.hint-container.tip a, +.hint-container.warning a, +.hint-container.caution a { + color: var(--color-link-light); + + html[data-theme="dark"] & { + color: var(--color-link); + } +} + +.vp-feature-item { + flex-basis: 21%; +} diff --git a/docs/api/appending-events.md b/docs/api/appending-events.md new file mode 100644 index 000000000..acbc089bc --- /dev/null +++ b/docs/api/appending-events.md @@ -0,0 +1,193 @@ +--- +order: 2 +--- + +# Appending events + +When you start working with KurrentDB, it is empty. The first meaningful operation is to add one or more events to the database using one of the available client SDKs. + +::: tip +Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. +::: + +## Append your first event + +The simplest way to append an event to KurrentDB is to create an `EventData` object and call `AppendToStream` method. + +```cs +var eventData = new EventData( + Uuid.NewUuid(), + "some-event", + "{\"id\": \"1\" \"value\": \"some value\"}"u8.ToArray() +); + +await client.AppendToStreamAsync( + "some-stream", + StreamState.NoStream, + new List { + eventData + } +); +``` + +`AppendToStream` takes a collection of `EventData`, which allows you to save more than one event in a single batch. + +Outside the example above, other options exist for dealing with different scenarios. + +::: tip +If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. +::: + +## Working with EventData + +Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **type**, and **data**. + +### eventId + +This takes the format of a `Uuid` and is used to uniquely identify the event you are trying to append. If two events with the same `Uuid` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. + +For example, the following code will only append a single event: + +```cs +var eventData = new EventData( + Uuid.NewUuid(), + "some-event", + "{\"id\": \"1\" \"value\": \"some value\"}"u8.ToArray() +); + +await client.AppendToStreamAsync( + "same-event-stream", + StreamState.Any, + new List { + eventData + } +); + +// attempt to append the same event again +await client.AppendToStreamAsync( + "same-event-stream", + StreamState.Any, + new List { + eventData + } +); +``` + +![Duplicate Event](./images/duplicate-event.png) + +### type + +Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. + +It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. + +### data + +Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. + +### metadata + +Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. + +### isJson + +Simple boolean field to tell KurrentDB if the event is stored as json, true by default. + +## Handling concurrency + +When appending events to a stream, you can supply a *stream state* or *stream revision*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. + +For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: + +```cs +var eventDataOne = new EventData( + Uuid.NewUuid(), + "some-event", + "{\"id\": \"1\" \"value\": \"some value\"}"u8.ToArray() +); + +var eventDataTwo = new EventData( + Uuid.NewUuid(), + "some-event", + "{\"id\": \"2\" \"value\": \"some other value\"}"u8.ToArray() +); + +await client.AppendToStreamAsync( + "no-stream-stream", + StreamState.NoStream, + new List { + eventDataOne + } +); + +// attempt to append the same event again +await client.AppendToStreamAsync( + "no-stream-stream", + StreamState.NoStream, + new List { + eventDataTwo + } +); +``` + +There are three available stream states: +- `Any` +- `NoStream` +- `StreamExists` + +This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. + +```cs +var clientOneRead = client.ReadStreamAsync( + Direction.Forwards, + "concurrency-stream", + StreamPosition.Start +); + +var clientOneRevision = (await clientOneRead.LastAsync()).Event.EventNumber.ToUInt64(); + +var clientTwoRead = client.ReadStreamAsync(Direction.Forwards, "concurrency-stream", StreamPosition.Start); +var clientTwoRevision = (await clientTwoRead.LastAsync()).Event.EventNumber.ToUInt64(); + +var clientOneData = new EventData( + Uuid.NewUuid(), + "some-event", + "{\"id\": \"1\" \"value\": \"clientOne\"}"u8.ToArray() +); + +await client.AppendToStreamAsync( + "no-stream-stream", + clientOneRevision, + new List { + clientOneData + } +); + +var clientTwoData = new EventData( + Uuid.NewUuid(), + "some-event", + "{\"id\": \"2\" \"value\": \"clientTwo\"}"u8.ToArray() +); + +await client.AppendToStreamAsync( + "no-stream-stream", + clientTwoRevision, + new List { + clientTwoData + } +); +``` + +## User credentials + +You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. + +```cs +await client.AppendToStreamAsync( + "some-stream", + StreamState.Any, + new[] { eventData }, + userCredentials: new UserCredentials("admin", "changeit"), + cancellationToken: cancellationToken +); +``` \ No newline at end of file diff --git a/docs/api/authentication.md b/docs/api/authentication.md new file mode 100644 index 000000000..3b432dfa8 --- /dev/null +++ b/docs/api/authentication.md @@ -0,0 +1,47 @@ +--- +title: Authentication +order: 7 +head: + - - title + - {} + - Authentication | .NET | Clients | Kurrent Docs +--- + +# Client x.509 certificate + + + +X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. + +## Prerequisites + +1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. +2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. + +## Connect using an x.509 certificate + +To connect using an x.509 certificate, you need to provide the certificate and +the private key to the client. If both username or password and certificate +authentication data are supplied, the client prioritizes user credentials for +authentication. The client will throw an error if the certificate and the key +are not both provided. + +The client supports the following parameters: + +| Parameter | Description | +|----------------|--------------------------------------------------------------------------------| +| `userCertFile` | The file containing the X.509 user certificate in PEM format. | +| `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | + +To authenticate, include these two parameters in your connection string or constructor when initializing the client: + +```cs +const string userCertFile = "/path/to/user.crt"; +const string userKeyFile = "/path/to/user.key"; + +var settings = EventStoreClientSettings.Create( + $"kurrentdb://localhost:2113/?tls=true&tlsVerifyCert=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}" +); + +await using var client = new KurrentDBClient(settings); +``` \ No newline at end of file diff --git a/docs/api/delete-stream.md b/docs/api/delete-stream.md new file mode 100644 index 000000000..79daeb2e0 --- /dev/null +++ b/docs/api/delete-stream.md @@ -0,0 +1,36 @@ +--- +order: 9 +head: + - - title + - {} + - Deleting Events | .NET | Clients | Kurrent Docs +--- + +# Deleting Events + +In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. +When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. + +## Soft delete + +Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, +handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. + +```csharp +await client.DeleteAsync(streamName, StreamState.Any); +``` + +::: note +Clicking the delete button in the UI performs a soft delete, +setting the TruncateBefore value to remove all events up to a certain point. +While this marks the events for deletion, actual removal occurs during the next scavenging process. +The stream can still be reopened by appending new events. +::: + +## Hard delete + +Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. + +```csharp +await client.TombstoneAsync(streamName, StreamState.Any); +``` \ No newline at end of file diff --git a/docs/api/getting-started.md b/docs/api/getting-started.md new file mode 100644 index 000000000..509ba8ffc --- /dev/null +++ b/docs/api/getting-started.md @@ -0,0 +1,137 @@ +--- +order: 1 +head: + - - title + - {} + - Getting Started | .NET | Clients | KurrentDB Docs +--- + +# Getting started + +Get started by connecting your application to KurrentDB. + +## Connecting to KurrentDB + +To connect your application to KurrentDB, instantiate and configure the client. + +::: tip Insecure clusters +All our GRPC clients are secure by default and must be configured to connect to an insecure server via [a connection string](#connection-string) or the client's configuration. +::: + +### Required packages + +Add the `KurrentDB.Client` package to your project: + +```bash +dotnet add package KurrentDB.Client --version "1.0.*" +``` + +### Connection string + +Each SDK has its own way of configuring the client, but the connection string can always be used. +The KurrentDB connection string supports two schemas: `kurrentdb://` for connecting to a single-node server, and `kurrentdb+discover://` for connecting to a multi-node cluster. The difference between the two schemas is that when using `kurrentdb://`, the client will connect directly to the node; with `kurrentdb+discover://` schema the client will use the gossip protocol to retrieve the cluster information and choose the right node to connect to. +Since version 22.10, kurrentdb supports gossip on single-node deployments, so `kurrentdb+discover://` schema can be used for connecting to any topology. + +The connection string has the following format: + +``` +kurrentdb+discover://admin:changeit@cluster.dns.name:2113 +``` + +There, `cluster.dns.name` is the name of a DNS `A` record that points to all the cluster nodes. Alternatively, you can list cluster nodes separated by comma instead of the cluster DNS name: + +``` +kurrentdb+discover://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 +``` + +There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. + +| Parameter | Accepted values | Default | Description | +|-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| +| `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | +| `connectionName` | Any string | None | Connection name | +| `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | +| `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | +| `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | +| `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | +| `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | +| `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | +| `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | +| `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | +| `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | +| `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | +| `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | + +When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. + +### Creating a client + +First, create a client and get it connected to the database. + +```cs +var client = new KurrentDBClient(KurrentDBClientSettings.Create("kurrentdb://admin:changeit@localhost:2113?tls=false&tlsVerifyCert=false")); +``` + +The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. + +### Creating an event + +You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. + +::: tip Server-side projections +User-defined server-side projections require events to be serialized in JSON format. + +We use JSON for serialization in the documentation examples. +::: + +The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. + +```cs +using System.Text.Json; + +var evt = new TestEvent { + EntityId = Guid.NewGuid().ToString("N"), + ImportantData = "I wrote my first event!" +}; + +var eventData = new EventData( + Uuid.NewUuid(), + "TestEvent", + JsonSerializer.SerializeToUtf8Bytes(evt) +); +``` + +### Appending events + +Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. + +In the snippet below, we append the event to the stream `some-stream`. + +```cs +await client.AppendToStreamAsync( + "some-stream", + StreamState.Any, + new[] { eventData }, + cancellationToken: cancellationToken +); +``` + +Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). + +### Reading events + +Finally, we can read events back from the `some-stream` stream. + +```cs +var result = client.ReadStreamAsync( + Direction.Forwards, + "some-stream", + StreamPosition.Start, + cancellationToken: cancellationToken +); + +var events = await result.ToListAsync(cancellationToken); +``` + +When you read events from the stream, you get a collection of `ResolvedEvent` structures. The event payload is returned as a byte array and needs to be deserialized. See more advanced scenarios in [reading events documentation](./reading-events.md). + diff --git a/docs/api/images/duplicate-event.png b/docs/api/images/duplicate-event.png new file mode 100644 index 0000000000000000000000000000000000000000..8f24748c564a42b47cab6a284555657bb00a60dc GIT binary patch literal 26730 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 S31)vFEXZ>;`C{33f&U9OBvs)6 literal 0 HcmV?d00001 diff --git a/docs/api/observability.md b/docs/api/observability.md new file mode 100644 index 000000000..146b22039 --- /dev/null +++ b/docs/api/observability.md @@ -0,0 +1,150 @@ +--- +order: 8 +head: + - - title + - {} + - Observability | .NET | Clients | Kurrent Docs +--- + +# Observability + +The .NET client provides observability capabilities through OpenTelemetry +integration. This enables you to monitor, trace, and troubleshoot your event +store operations with distributed tracing support. + +## Prerequisites + +You'll need to install exporters for your chosen observability platform: + +```bash +# For console output +dotnet add package OpenTelemetry.Exporter.Console + +# For Jaeger +dotnet add package OpenTelemetry.Exporter.Jaeger + +# For OTLP (OpenTelemetry Protocol) +dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol + +# For Seq +dotnet add package Seq.Extensions.Logging +``` + +## Basic Configuration + +Configure instrumentation using the `AddKurrentDBClientInstrumentation()` +extension method. Here's a minimal setup: + +```csharp {15} +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +const string serviceName = "my-kurrentdb-app"; + +var host = Host.CreateDefaultBuilder() + .ConfigureServices((_, services) => + { + services.AddOpenTelemetry() + .ConfigureResource(builder => builder.AddService(serviceName)) + .WithTracing(tracerBuilder => tracerBuilder + .AddKurrentDBClientInstrumentation() + .AddConsoleExporter() + ); + }) + .Build(); + +await host.RunAsync(); +``` + +## Trace Exporters + +OpenTelemetry supports various exporters to send trace data to different +observability platforms. You can find a list of available exporters in the +[OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter&language=dotnet). + +You can configure multiple exporters simultaneously: + +```csharp {10-18} +using OpenTelemetry.Exporter; + +var host = Host.CreateDefaultBuilder() + .ConfigureServices((_, services) => + { + services.AddOpenTelemetry() + .ConfigureResource(builder => builder.AddService("my-kurrentdb-app")) + .WithTracing(tracerBuilder => tracerBuilder + .AddKurrentDBClientInstrumentation() + .AddConsoleExporter() + .AddJaegerExporter(options => + { + options.Endpoint = new Uri("http://localhost:14268/api/traces"); + }) + .AddOtlpExporter(options => + { + options.Endpoint = new Uri("http://localhost:4318/v1/traces"); + }) + ); + }) + .Build(); +``` + +For detailed configuration options, refer to the +[OpenTelemetry .NET documentation](https://opentelemetry.io/docs/languages/dotnet/). + +## Understanding Traces + +### What Gets Traced + +The .NET client currently creates traces for append, catch-up and persistent +subscription operations. + +### Trace Attributes + +Each trace includes metadata to help with debugging and monitoring: + +| Attribute | Description | Example | +| ------------------------- | -------------------------------------- | ------------------------------------- | +| `db.user` | Database user name | `admin` | +| `db.system` | Database system identifier | `kurrentdb` | +| `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | +| `db.kurrentdb.stream` | Stream name or identifier | `user-events-123` | +| `server.address` | KurrentDB server address | `localhost` | +| `server.port` | KurrentDB server port | `2113` | +| `otel.status_code` | Status code for the operation | `UNSET`, `OK`, `ERROR` | +| `otel.status_description` | Status of a span | | +| `exception.type` | Exception type if an error occurred | | +| `exception.message` | Exception message if an error occurred | | +| `exception.stacktrace` | Stack trace of the exception | | + +### Sample Trace Output + +Here's an example trace from a stream append operation: + +```bash +Activity.TraceId: 8da04787239dbb85c1f9c6fba1b1f0d6 +Activity.SpanId: 4352ec4a66a20b95 +Activity.TraceFlags: Recorded +Activity.ActivitySourceName: kurrentdb +Activity.DisplayName: streams.append +Activity.Kind: Client +Activity.StartTime: 2024-05-29T06:50:41.2519016Z +Activity.Duration: 00:00:00.1500707 +Activity.Tags: + db.kurrentdb.stream: example-stream + server.address: localhost + server.port: 2113 + db.system: kurrentdb + db.operation: streams.append + event.count: 3 +StatusCode: Ok +Resource associated with Activity: + service.name: my-kurrentdb-app + service.instance.id: 7316ef20-c354-4e64-97da-c1b99c2c28b0 + service.version: 1.0.0 + deployment.environment: production + telemetry.sdk.name: opentelemetry + telemetry.sdk.language: dotnet + telemetry.sdk.version: 1.9.0 +``` diff --git a/docs/api/persistent-subscriptions.md b/docs/api/persistent-subscriptions.md new file mode 100644 index 000000000..e8e83ff4b --- /dev/null +++ b/docs/api/persistent-subscriptions.md @@ -0,0 +1,226 @@ +--- +order: 5 +head: + - - title + - {} + - Persistent Subscriptions | .NET | Clients | Kurrent Docs +--- + +# Persistent Subscriptions + +Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: +- The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. +- It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. + +Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. + +You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). + +## Creating a Subscription Group + +The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. + +The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. + +### Subscribing to a Specific Stream + +```cs +var settings = new PersistentSubscriptionSettings(); +await client.CreateToStreamAsync("example-stream", "subscription-group", settings); +``` + +### Subscribing to `$all` + +```cs +var settings = new PersistentSubscriptionSettings(); +await client.CreateToAllAsync("subscription-group", StreamFilter.Prefix("user"), settings); +``` + +::: note +As from EventStoreDB 21.10, the ability to subscribe to `$all` supports +[server-side filtering](subscriptions.md#server-side-filtering). You can create +a subscription group for `$all` similarly to how you would for a specific +stream: +::: + +## Connecting a consumer + +Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. + +The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. + +### Connecting to one stream + +The code below shows how to connect to an existing subscription group for a specific stream: + +```cs +await using var subscription = client.SubscribeToStream( + "example-stream", + "subscription-group", + cancellationToken: ct +); + +await foreach (var message in subscription.Messages) { + switch (message) { + case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): + Console.WriteLine($"Subscription {subscriptionId} to stream started"); + break; + case PersistentSubscriptionMessage.Event(var resolvedEvent, _): + await HandleEvent(resolvedEvent); + await subscription.Ack(resolvedEvent); + break; + } +} +``` + +| Parameter | Description | +|:----------------------|:---------------------------------------------------------------------------------------------| +| `stream` | The stream the persistent subscription is on. | +| `groupName` | The name of the subscription group to subscribe to. | +| `eventAppeared` | The action to call when an event arrives over the subscription. | +| `subscriptionDropped` | The action to call if the subscription is dropped. | +| `bufferSize` | The number of in-flight messages this client is allowed. **Default: 10** | + +### Connecting to $all + +The code below shows how to connect to an existing subscription group for `$all`: + +```cs +await using var subscription = client.SubscribeToAll("subscription-group", cancellationToken: ct); + +await foreach (var message in subscription.Messages) { + switch (message) { + case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): + Console.WriteLine($"Subscription {subscriptionId} to stream started"); + break; + case PersistentSubscriptionMessage.Event(var resolvedEvent, _): + await HandleEvent(resolvedEvent); + break; + } +} +``` + +The `SubscribeToAllAsync` method is identical to the `SubscribeToStreamAsync` method, except that you don't need to specify a stream name. + +## Acknowledgements + +Clients must acknowledge (or not acknowledge) messages in the competing consumer model. + +If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. + +```cs +await using var subscription = client.SubscribeToStream( + "test-stream", + "subscription-group", + cancellationToken: ct +); + +await foreach (var message in subscription.Messages) { + switch (message) { + case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): + Console.WriteLine($"Subscription {subscriptionId} to stream with manual acks started"); + break; + case PersistentSubscriptionMessage.Event(var resolvedEvent, _): + try { + await HandleEvent(resolvedEvent); + await subscription.Ack(resolvedEvent); + } catch (UnrecoverableException ex) { + await subscription.Nack(PersistentSubscriptionNakEventAction.Park, ex.Message, resolvedEvent); + } + break; + } +} +``` + +The _Nack event action_ describes what the server should do with the message: + +| Action | Description | +|:----------|:---------------------------------------------------------------------| +| `Unknown` | The client does not know what action to take. Let the server decide. | +| `Park` | Park the message and do not resend. Put it on poison queue. | +| `Retry` | Explicitly retry the message. | +| `Skip` | Skip this message do not resend and do not put in poison queue. | + +## Consumer strategies + +When creating a persistent subscription, you can choose between a number of consumer strategies. + +### RoundRobin (default) + +Distributes events to all clients evenly. If the client `bufferSize` is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. + +This strategy provides equal load balancing between all consumers in the group. + +### DispatchToSingle + +Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process repeats. + +This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. + +### Pinned + +For use with an indexing projection such as the system `$by_category` projection. + +KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. + +The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. + +## Updating a subscription group + +You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. + +```cs +var settings = new PersistentSubscriptionSettings( + resolveLinkTos: true, + checkPointLowerBound: 20 +); + +await client.UpdateToStreamAsync("example-stream", "subscription-group", settings); +``` + +| Parameter | Description | +|:--------------|:----------------------------------------------------| +| `stream` | The stream the persistent subscription is on. | +| `groupName` | The name of the subscription group to update. | +| `settings` | The settings to use when creating the subscription. | + +## Persistent subscription settings + +Both the `Create` and `Update` methods take some settings for configuring the persistent subscription. + +The following table shows the configuration options you can set on a persistent subscription. + +| Option | Description | Default | +|:------------------------|:----------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------| +| `ResolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | +| `StartFrom` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | +| `ExtraStatistics` | Whether to track latency statistics on this subscription. | `false` | +| `MessageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | +| `MaxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | +| `LiveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | +| `ReadBatchSize` | The number of events read at a time when paging through history. | `20` | +| `HistoryBufferSize` | The number of events to cache when paging through history. | `500` | +| `CheckPointAfter` | The amount of time to try to checkpoint after. | `2` seconds | +| `MinCheckPointCount` | The minimum number of messages to process before a checkpoint may be written. | `10` | +| `MaxCheckPointCount` | The maximum number of messages not checkpointed before forcing a checkpoint. | `1000` | +| `MaxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | +| `NamedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | + +## Deleting a subscription group + +Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. + +```cs +try { + await client.DeleteToStreamAsync("example-stream", "subscription-group"); +} catch (PersistentSubscriptionNotFoundException) { + Console.WriteLine("Subscription group does not exist."); +} catch (Exception ex) { + Console.WriteLine($"Subscription to stream delete error: {ex.GetType()} {ex.Message}"); +} +``` + +| Parameter | Description | +|:--------------|:-----------------------------------------------| +| `stream` | The stream the persistent subscription is on. | +| `groupName` | The name of the subscription group to delete. | diff --git a/docs/api/projections.md b/docs/api/projections.md new file mode 100644 index 000000000..e7df15363 --- /dev/null +++ b/docs/api/projections.md @@ -0,0 +1,389 @@ +--- +order: 6 +title: Projections +head: + - - title + - {} + - Projections | .NET | Clients | Kurrent Docs +--- + +# Projection management + +The client provides a way to manage projections in KurrentDB. + +For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). + +## Creating a client + +Projection management operations are exposed through the dedicated client. + +```cs +var settings = KurrentDBClientSettings.Create(connection); +settings.ConnectionName = "Projection management client"; +settings.DefaultCredentials = new UserCredentials("admin", "changeit"); +var managementClient = new KurrentDBProjectionManagementClient(settings); +``` + +## Create a projection + +Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. +Projections have explicit names, and you can enable or disable them via this name. + +```cs +const string js = """ + fromAll() + .when({ + $init: function() { + return { + count: 0 + }; + }, + $any: function(s, e) { + s.count += 1; + } + }) + .outputState(); +"""; + +await managementClient.CreateContinuousAsync("count-events", js); +``` + +Trying to create projections with the same name will result in an error: + +```cs +var name = "count-events"; + +await managementClient.CreateContinuousAsync(name, js); +try { + await managementClient.CreateContinuousAsync(name, js); +} +catch (RpcException e) when (e.StatusCode is StatusCode.AlreadyExists) { + Console.WriteLine(e.Message); +} +catch (RpcException e) when (e.Message.Contains("Conflict")) { // will be removed in a future release + var format = $"{name} already exists"; + Console.WriteLine(format); +} +``` + +## Restart the subsystem + +It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. + +```cs +await managementClient.RestartSubsystemAsync(); +``` + +## Enable a projection + +Enables an existing projection by name. +Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. +You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). + +```cs +await managementClient.EnableAsync("$by_category"); +``` + +You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: + + ```cs +try { + await managementClient.EnableAsync("projection that does not exists"); +} +catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { + Console.WriteLine(e.Message); +} +catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release + Console.WriteLine(e.Message); +} +``` + +## Disable a projection + +Disables a projection, this will save the projection checkpoint. +Once disabled, the projection will not process events even after restarting the server or the projection subsystem. +You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). + +```cs +await managementClient.DisableAsync("$by_category"); +``` + +You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: + +```cs +try { + await managementClient.DisableAsync("projection that does not exists"); +} +catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { + Console.WriteLine(e.Message); +} +catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release + Console.WriteLine(e.Message); +} +``` + +## Delete a projection + +This feature is currently not supported by the client. + +## Abort a projection + +Aborts a projection, this will not save the projection's checkpoint. + +```cs +await managementClient.AbortAsync("countEvents_Abort"); +``` + +You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: + +```cs +try { + await managementClient.AbortAsync("projection that does not exists"); +} +catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { + Console.WriteLine(e.Message); +} +catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release + Console.WriteLine(e.Message); +} +``` + +## Reset a projection + +Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. + +```cs +await managementClient.ResetAsync("countEvents_Reset"); +``` + +Resetting a projection that does not exist will result in an error. + +```cs +try { + await managementClient.ResetAsync("projection that does not exists"); +} +catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { + Console.WriteLine(e.Message); +} +catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release + Console.WriteLine(e.Message); +} +``` + +## Update a projection + +Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. + +```cs +const string js = """ +fromAll() + .when({ + $init: function() { + return { + count: 0 + }; + }, + $any: function(s, e) { + s.count += 1; + } + }) + .outputState(); +"""; + +var name = "count-events"; + +await managementClient.CreateContinuousAsync(name, "fromAll().when()"); +await managementClient.UpdateAsync(name, js); +``` + +You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: + +```cs +try { + await managementClient.UpdateAsync("Update Not existing projection", "fromAll().when()"); +} +catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { + Console.WriteLine(e.Message); +} +catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release + Console.WriteLine("'Update Not existing projection' does not exists and can not be updated"); +} +``` + +## List all projections + +Returns a list of all projections, user defined & system projections. +See the [projection details](#projection-details) section for an explanation of the returned values. + +```cs +var details = managementClient.ListAllAsync(); + +await foreach (var detail in details) + Console.WriteLine( + $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" + ); +``` + +## List continuous projections + +Returns a list of all continuous projections. +See the [projection details](#projection-details) section for an explanation of the returned values. + +```cs +var details = managementClient.ListContinuousAsync(); +await foreach (var detail in details) + Console.WriteLine( + $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" + ); +``` + +## Get status + +Gets the status of a named projection. +See the [projection details](#projection-details) section for an explanation of the returned values. + +```cs +const string js = """ +fromAll() + .when({ + $init: function() { + return { count: 0 }; + }, + $any: function(state, event) { + state.count += 1; + } + }) + .outputState(); +"""; + +var name = "count-events"; + +await managementClient.CreateContinuousAsync(name, js); +var status = await managementClient.GetStatusAsync(name); +Console.WriteLine( + $@"{status?.Name}, {status?.Status}, {status?.CheckpointStatus}, {status?.Mode}, {status?.Progress}" +); +``` + +## Get state + +Retrieves the state of a projection. + +```cs +const string js = """ +fromAll() + .when({ + $init: function() { + return { count: 0 }; + }, + $any: function(s, e) { + s.count += 1; + } + }) + .outputState(); +"""; + +var name = $"count-events"; + +await managementClient.CreateContinuousAsync(name, js); + +//give it some time to process and have a state. +await Task.Delay(500); + +var stateDocument = await managementClient.GetStateAsync(name); +var result = await managementClient.GetStateAsync(name); + +Console.WriteLine(DocToString(stateDocument)); +Console.WriteLine(result); + +static async Task DocToString(JsonDocument d) { + await using var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false }); + d.WriteTo(writer); + await writer.FlushAsync(); + return Encoding.UTF8.GetString(stream.ToArray()); +} +``` + +## Get result + +Retrieves the result of the named projection and partition. + +```cs +const string js = """ +fromAll() + .when({ + $init: function() { + return { count: 0 }; + }, + $any: function(s, e) { + s.count += 1; + } + }) + .outputState(); +"""; + +var name = "count-events"; + +await managementClient.CreateContinuousAsync(name, js); + +await Task.Delay(500); //give it some time to have a result. + +// Results are retrieved either as JsonDocument or a typed result +var document = await managementClient.GetResultAsync(name); +var result = await managementClient.GetResultAsync(name); + +Console.WriteLine(DocToString(document)); +Console.WriteLine(result); + +static string DocToString(JsonDocument d) { + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false }); + d.WriteTo(writer); + writer.Flush(); + return Encoding.UTF8.GetString(stream.ToArray()); +} +``` + +## Projection Details + +The `ListAllAsync`, `ListContinuousAsync`, and `GetStatusAsync` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: + +| Field | Description | +|--------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Name`, `EffectiveName` | The name of the projection | +| `Status` | A human readable string of the current statuses of the projection (see below) | +| `StateReason` | A human readable string explaining the reason of the current projection state | +| `CheckpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | +| `Mode` | `Continuous`, `OneTime` , `Transient` | +| `CoreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | +| `Progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | +| `WritesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | +| `ReadsInProgress` | The number of read requests currently in progress | +| `PartitionsCached` | The number of cached projection partitions | +| `Position` | The Position of the last processed event | +| `LastCheckpoint` | The Position of the last checkpoint of this projection | +| `EventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | +| `BufferedEvents` | The number of events in the projection read buffer | +| `WritePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | +| `WritePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | +| `Version` | This is used internally, the version is increased when the projection is edited or reset | +| `Epoch` | This is used internally, the epoch is increased when the projection is reset | + +The `Status` string is a combination of the following values. +The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped + +| Value | Description | +|--------------------|-------------------------------------------------------------------------------------------------------------------------| +| Running | The projection is running and processing events | +| Stopped | The projection is stopped and is no longer processing new events | +| Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | +| Initial | This is the initial state, before the projection is fully initialised | +| Suspended | The projection is suspended and will not process events, this happens while stopping the projection | +| LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | +| StateLoaded | The state of the projection is loaded, this happens while the projection is starting | +| Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | +| FaultedStopping | This happens before the projection is stopped due to an error in the projection | +| Stopping | The projection is being stopped | +| CompletingPhase | This happens while the projection is stopping | +| PhaseCompleted | This happens while the projection is stopping | diff --git a/docs/api/reading-events.md b/docs/api/reading-events.md new file mode 100644 index 000000000..f1093975a --- /dev/null +++ b/docs/api/reading-events.md @@ -0,0 +1,193 @@ +--- +order: 3 +head: + - - title + - {} + - Reading Events | .NET | Clients | Kurrent Docs +--- + +# Reading Events + +There are two options for reading events from KurrentDB. You can either read +from an individual stream, or read from the `$all` stream, which will return all +events in the store. + +Each event in KurrentDB belongs to an individual stream. When reading events, pick the name of the stream from which you want to read the events and choose whether to read the stream forwards or backwards. + +All events have a `StreamPosition` and a `Position`. `StreamPosition` is a *big int* (unsigned 64-bit integer) and represents the place of the event in the stream. `Position` is the event's logical position, and is represented by `CommitPosition` and a `PreparePosition`. Note that when reading events you will supply a different "position" depending on whether you are reading from an individual stream or the `$all` stream. + +:::tip +Check [connecting to KurrentDB instructions](getting-started.md#required-packages) to learn how to configure and use the client SDK. +::: + +## Reading from a stream + +You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. + +### Reading forwards + +The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can either be a *stream position* `Start` or a *big int* (unsigned 64-bit integer): + + +```cs +var events = client.ReadStreamAsync(Direction.Forwards, "example-stream", StreamPosition.Start); +``` + +This will return an enumerable that can be iterated on: + +```cs +await foreach (var @event in events) + Console.WriteLine(Encoding.UTF8.GetString(@event.Event.Data.ToArray())); +``` + +There are a number of additional arguments you can provide when reading a stream, listed below. + +#### maxCount + +Passing in the max count will limit the number of events returned. + +#### resolveLinkTos + +When using projections to create new events, you can set whether the generated events are pointers to existing events. Setting this value to `true` tells KurrentDB to return the event as well as the event linking to it. + +#### configureOperationOptions + +You can use the `configureOperationOptions` argument to provide a function that will customise settings for each operation. + +#### userCredentials + +The `userCredentials` argument is optional. It is used to override the default credentials specified when creating the client instance. + +```cs +var result = client.ReadStreamAsync( + Direction.Forwards, + "example-stream", + StreamPosition.Start, + userCredentials: new UserCredentials("admin", "changeit"), + cancellationToken: cancellationToken +); +``` + +### Reading from a revision + +Instead of providing the `StreamPosition` you can also provide a specific stream revision as a *big int* (unsigned 64-bit integer). + +```cs +Console.WriteLine(events.FirstStreamPosition); +Console.WriteLine(events.LastStreamPosition); +``` + +### Reading backwards + +In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the *stream position* to the end: + +```cs +var events = client.ReadStreamAsync(Direction.Backwards, "example-stream", StreamPosition.End); + +await foreach (var e in events) + Console.WriteLine(Encoding.UTF8.GetString(e.Event.Data.ToArray())); +``` + +:::tip +Read one event backwards to find the last position in the stream. +::: + +### Checking if the stream exists + +Reading a stream returns a `ReadStreamResult`, which contains a property `ReadState`. This property can have the value `StreamNotFound` or `Ok`. + +It is important to check the value of this field before attempting to iterate an empty stream, as it will throw an exception. + +For example: + +```cs +var result = client.ReadStreamAsync(Direction.Forwards, "example-stream", 10, 20); + +if (await result.ReadState == ReadState.StreamNotFound) return; + +await foreach (var e in result) + Console.WriteLine(Encoding.UTF8.GetString(e.Event.Data.ToArray())); +``` + +## Reading from the $all stream + +Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. + +### Reading forwards + +The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log postion can either be a *stream position* `Start` or a *big int* (unsigned 64-bit integer): + +```cs +var events = client.ReadAllAsync(Direction.Forwards, Position.Start); +``` + +You can iterate asynchronously through the result: + +```cs +await foreach (var e in events) + Console.WriteLine(Encoding.UTF8.GetString(e.Event.Data.ToArray())); +``` + +There are a number of additional arguments you can provide when reading the `$all` stream. + +#### maxCount + +Passing in the max count allows you to limit the number of events that returned. + +#### resolveLinkTos + +When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. + +```cs +var result = client.ReadAllAsync( + Direction.Forwards, + Position.Start, + resolveLinkTos: true, + cancellationToken: cancellationToken +); +``` + +#### configureOperationOptions + +This argument is generic setting class for all operations that can be set on all operations executed against KurrentDB. + +#### userCredentials +The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. + +```cs +var result = client.ReadAllAsync( + Direction.Forwards, + Position.Start, + userCredentials: new UserCredentials("admin", "changeit"), + cancellationToken: cancellationToken +); +``` + +### Reading backwards + +In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *position* to the end: + +```cs +var events = client.ReadAllAsync(Direction.Backwards, Position.End); +``` + +:::tip +Read one event backwards to find the last position in the `$all` stream. +::: + +### Handling system events + +KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. + +All system events begin with `$` or `$$` and can be easily ignored by checking the `EventType` property. + +```cs +var events = client.ReadAllAsync(Direction.Forwards, Position.Start); + +await foreach (var e in events) { + if (e.Event.EventType.StartsWith("$")) continue; + + Console.WriteLine(Encoding.UTF8.GetString(e.Event.Data.ToArray())); +} +``` + diff --git a/docs/api/subscriptions.md b/docs/api/subscriptions.md new file mode 100644 index 000000000..a6a9fc0f1 --- /dev/null +++ b/docs/api/subscriptions.md @@ -0,0 +1,478 @@ +--- +order: 4 +head: + - - title + - {} + - Catch-up Subscriptions | .NET | Clients | Kurrent Docs +--- + +# Catch-up Subscriptions + +Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. + +You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. + +If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. + +:::tip +Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. +::: + +## Subscribing from the start + +If you need to process all the events in the store, including historical events, you'll need to subscribe from the beginning. You can either subscribe to receive events from a single stream or subscribe to `$all` if you need to process all events in the database. + +### Subscribing to a stream + +The simplest stream subscription looks like the following : + +```cs +await using var subscription = client.SubscribeToStream( + "example-stream", + FromStream.Start, + cancellationToken: ct +); + +await foreach (var message in subscription.Messages.WithCancellation(ct)) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + break; + } +} +``` + +The provided handler will be called for every event in the stream. + +When you subscribe to a stream with link events, for example the `$ce` category stream, you need to set `resolveLinkTos` to `true`. Read more about it [below](#resolving-link-to-s). + +### Subscribing to `$all` + +Subscribing to `$all` is similar to subscribing to a single stream. The handler will be called for every event appended after the starting position. + +```cs +await using var subscription = client.SubscribeToAll( + FromAll.Start, + cancellationToken: ct +); + +await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + break; + } +} +``` + +## Subscribing from a specific position + +The previous examples subscribed to the stream from the beginning. That subscription invoked the handler for every event in the stream before waiting for new events. + +Both stream and $all subscriptions accept a starting position if you want to read from a specific point onward. If events already exist at the position you subscribe to, they will be read on the server side and sent to the subscription. + +Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. + +::: warning +The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. +::: + +### Subscribing to a stream + +To subscribe to a stream from a specific position, you must provide a *stream position*. This can be `Start`, `End` or a *big int* (unsigned 64 bit integer) position. + +The following subscribes to the stream `example-stream` at position `20`, this means that events `21` and onward will be handled: + +```cs +await using var subscription = client.SubscribeToStream( + "example-stream", + FromStream.After(StreamPosition.FromInt64(20)), + cancellationToken: ct +); + +await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + break; + } +} +``` + +### Subscribing to $all + +Subscribing to the `$all` stream is similar to subscribing to a regular stream. The difference is how to specify the starting position. For the `$all` stream, provide a `Position` structure that consists of two big integers: the prepare and commit positions. Use `Start`, `End`, or create a `Position` from specific commit and prepare values. + +The corresponding `$all` subscription will subscribe from the event after the one at commit position `1056` and prepare position `1056`. + +Please note that this position will need to be a legitimate position in `$all`. + +```cs +var result = await client.AppendToStreamAsync( + "example-stream", + StreamState.NoStream, + [ + new EventData(Uuid.NewUuid(), "-", ReadOnlyMemory.Empty) + ] +); + +await using var subscription = client.SubscribeToAll( + FromAll.After(result.LogPosition), + cancellationToken: ct +); + +await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + break; + } +} +``` + +## Subscribing to a stream for live updates + +You can subscribe to a stream to get live updates by subscribing to the end of the stream: + +```cs +await using var subscription = client.SubscribeToStream( + "example-stream", + FromStream.End, + cancellationToken: ct +); + +await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + break; + } +} +``` + +And the same works with `$all` : + +```cs +var subscription = client.SubscribeToAll(FromAll.End, cancellationToken: ct); + +await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + break; + } +} +``` + +This will not read through the history of the stream but will notify the handler when a new event appears in the respective stream. + +Keep in mind that when you subscribe to a stream from a specific position, as described [above](#subscribing-from-a-specific-position), you will also get live updates after your subscription catches up (processes all the historical events). + +## Resolving link-to events + +Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. + +::: tip +[Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. +::: + +When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: + +```cs +await using var subscription = client.SubscribeToStream( + "$et-myEventType", + FromStream.Start, + resolveLinkTos: true, + cancellationToken: ct +); + +await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + break; + } +} +``` + +## Dropped subscriptions + +When a subscription stops or experiences an error, it will be dropped. The subscription provides a `subscriptionDropped` callback, which will get called when the subscription breaks. + +The `subscriptionDropped` callback allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. + +The possible reasons for a subscription to drop are: + +| Reason | Why it might happen | +|:------------------|:---------------------------------------------------------------------------------------------------------------------| +| `Disposed` | The client canceled or disposed of the subscription. | +| `SubscriberError` | An error occurred while handling an event in the subscription handler. | +| `ServerError` | An error occurred on the server, and the server closed the subscription. Check the server logs for more information. | + +Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. + +### Handling subscription drops + +An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: + +```cs +var checkpoint = await ReadStreamCheckpointAsync() switch { + null => FromStream.Start, + var position => FromStream.After(position.Value) +}; + +try { + await using var subscription = client.SubscribeToStream( + "example-stream", + checkpoint, + cancellationToken: ct + ); + + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + checkpoint = FromStream.After(evnt.OriginalEventNumber); + break; + } + } +} catch (OperationCanceledException) { + Console.WriteLine($"Subscription was canceled."); +} catch (ObjectDisposedException) { + Console.WriteLine($"Subscription was canceled by the user."); +} catch (Exception ex) { + Console.WriteLine($"Subscription was dropped: {ex}"); + goto Subscribe; +} +``` + +When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: + +```cs +var checkpoint = await ReadCheckpointAsync() switch { + null => FromAll.Start, + var position => FromAll.After(position.Value) +}; + +try { + await using var subscription = client.SubscribeToAll( + checkpoint, + cancellationToken: ct); + await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + if (evnt.OriginalPosition is not null) { + checkpoint = FromAll.After(evnt.OriginalPosition.Value); + } + + break; + } + } +} catch (OperationCanceledException) { + Console.WriteLine($"Subscription was canceled."); +} catch (ObjectDisposedException) { + Console.WriteLine($"Subscription was canceled by the user."); +} catch (Exception ex) { + Console.WriteLine($"Subscription was dropped: {ex}"); + goto Subscribe; +} +``` + +## User credentials + +The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. + +The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. + +```cs +await using var subscription = client.SubscribeToAll( + FromAll.Start, + userCredentials: new UserCredentials("admin", "changeit"), + cancellationToken: ct +); + +await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + + break; + } +} +``` + +## Server-side filtering + +KurrentDB allows you to filter the events whilst subscribing to the `$all` stream to only receive the events you care about. + +You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. + +::: tip +Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. +::: + +A simple stream prefix filter looks like this: + +```cs +var filter = new SubscriptionFilterOptions(StreamFilter.Prefix("test-", "other-")); + +await using var subscription = client.SubscribeToAll( + FromAll.Start, + filterOptions: filter, + cancellationToken: ct +); + +await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + await HandleEvent(evnt); + + break; + case StreamMessage.AllStreamCheckpointReached(var position): + Console.WriteLine($"Checkpoint reached: {position}"); + break; + } +} +``` + +The filtering API is described more in-depth in the [filtering section](subscriptions.md#server-side-filtering). + +### Filtering out system events + +There are events in KurrentDB called system events. These are prefixed with a `$` and under most circumstances you won't care about these. They can be filtered out by passing in a `SubscriptionFilterOptions` when subscribing to the `$all` stream. + +```cs +await using var subscription = client.SubscribeToAll( + FromAll.Start, + filterOptions: new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()) +); + +await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var e): + Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.CommitPosition}"); + break; + } +} +``` + +::: tip +`$stats` events are no longer stored in KurrentDB by default so there won't be as many `$` events as before. +::: + +### Filtering by event type + +If you only want to subscribe to events of a given type, there are two options. You can either use a regular expression or a prefix. + +#### Filtering by prefix + +If you want to filter by prefix, pass in a `SubscriptionFilterOptions` to the subscription with an `EventTypeFilter.Prefix`. + +```cs +var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-")); +``` + +This will only subscribe to events with a type that begin with `customer-`. + +#### Filtering by regular expression + +It might be advantageous to provide a regular expression when you want to subscribe to multiple event types. + +```cs +var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.RegularExpression("^user|^company")); +``` + +This will subscribe to any event that begins with `user` or `company`. + +### Filtering by stream name + +To subscribe to a stream by name, choose either a regular expression or a prefix. + +#### Filtering by prefix + +If you want to filter by prefix, pass in a `SubscriptionFilterOptions` to the subscription with an `StreamFilter.Prefix`. + +```cs +var filterOptions = new SubscriptionFilterOptions(StreamFilter.Prefix("user-")); +``` + +This will only subscribe to all streams with a name that begins with `user-`. + +#### Filtering by regular expression + +To subscribe to multiple streams, use a regular expression. + +```cs +var filterOptions = new SubscriptionFilterOptions(StreamFilter.RegularExpression("^account|^savings")); +``` + +This will subscribe to any stream with a name that begins with `account` or `savings`. + +## Checkpointing + +When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. + +### What is a checkpoint? + +A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: +- Recover from crashes by reading the checkpoint and resuming from that position +- Avoid reprocessing all events from the start + +To create a checkpoint, store the event's commit or prepare position. + +::: warning +If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. +::: + +### Updating checkpoints at regular intervals +The client SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. + +```cs +var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()); + +await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); + +await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var e): + Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.CommitPosition}"); + break; + case StreamMessage.AllStreamCheckpointReached(var p): + // Save commit position to a persistent store as a checkpoint + Console.WriteLine($"checkpoint taken at {p.CommitPosition}"); + break; + } +} +``` + +By default, the checkpoint notification is sent after every 32 non-system events processed from $all. + +### Configuring the checkpoint interval +You can adjust the checkpoint interval to change how often the client is notified. + +```cs +var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents(), 1000); +``` + +By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. + +::: info +The checkpoint interval parameter configures the database to notify the client after `n` * 32 number of events where `n` is defined by the parameter. + +For example: +- If `n` = 1, a checkpoint notification is sent every 32 events. +- If `n` = 2, the notification is sent every 64 events. +- If `n` = 3, it is sent every 96 events, and so on. +::: diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 000000000..14f729c51 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,54 @@ +{ + "name": "docs", + "version": "1.0.0", + "private": true, + "type": "module", + "author": "Kurrent Inc", + "devDependencies": { + "@babel/cli": "^7.24.8", + "@babel/core": "^7.24.9", + "@babel/preset-env": "^7.24.8", + "@babel/preset-typescript": "^7.24.7", + "@types/fs-extra": "^11.0.4", + "@types/markdown-it": "^14.1.1", + "degit": "2.8.4", + "del": "5.1.0", + "dotenv": "10.0.0", + "eslint": "^8.57.0", + "eslint-config-vuepress": "^4.10.1", + "eslint-config-vuepress-typescript": "^4.10.1", + "fs-extra": "^11.2.0", + "markdown-it": "^14.1.0", + "prettier": "2.3.2", + "sass": "^1.84.0", + "shx": "0.3.3", + "stylus": "^0.56.0", + "tsconfig-vuepress": "^4.5.0", + "typescript": "^5.5.3", + "vite-plugin-vue-devtools": "^7.3.6", + "@vuepress/plugin-search": "^2.0.0-rc.74", + "@mdit/plugin-dl": "^0.13.0" + }, + "dependencies": { + "@rollup/plugin-alias": "^3.1.9", + "@vuelidate/core": "^2.0.3", + "@vuepress/bundler-vite": "2.0.0-rc.19", + "vuepress-theme-hope": "^2.0.0-rc.71", + "sass-loader": "^15.0.0", + "vue": "^3.4.30", + "vue-router": "^4.4.0", + "vuepress": "2.0.0-rc.19", + "iconify-icon": "^2.1.0", + "mermaid": "^11.3.0" + }, + "scripts": { + "preinstall": "npx only-allow pnpm", + "dev": "vuepress-vite dev .", + "build": "vuepress-vite build .", + "update-package": "pnpm dlx vp-update" + }, + "engines": { + "node": ">=18.19.0" + }, + "packageManager": "pnpm@9.10.0+sha512.73a29afa36a0d092ece5271de5177ecbf8318d454ecd701343131b8ebc0c1a91c487da46ab77c8e596d6acf1461e3594ced4becedf8921b074fbd8653ed7051c" +} diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 000000000..755a706cd --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,9767 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@rollup/plugin-alias': + specifier: ^3.1.9 + version: 3.1.9(rollup@4.34.6) + '@vuelidate/core': + specifier: ^2.0.3 + version: 2.0.3(vue@3.4.31(typescript@5.5.3)) + '@vuepress/bundler-vite': + specifier: 2.0.0-rc.19 + version: 2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3) + iconify-icon: + specifier: ^2.1.0 + version: 2.1.0 + mermaid: + specifier: ^11.3.0 + version: 11.3.0 + sass-loader: + specifier: ^15.0.0 + version: 15.0.0(sass@1.84.0) + vue: + specifier: ^3.4.30 + version: 3.4.31(typescript@5.5.3) + vue-router: + specifier: ^4.4.0 + version: 4.4.0(vue@3.4.31(typescript@5.5.3)) + vuepress: + specifier: 2.0.0-rc.19 + version: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + vuepress-theme-hope: + specifier: ^2.0.0-rc.71 + version: 2.0.0-rc.71(@vuepress/plugin-search@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))))(katex@0.16.11)(markdown-it@14.1.0)(mermaid@11.3.0)(sass-loader@15.0.0(sass@1.84.0))(sass@1.84.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + devDependencies: + '@babel/cli': + specifier: ^7.24.8 + version: 7.24.8(@babel/core@7.25.2) + '@babel/core': + specifier: ^7.24.9 + version: 7.25.2 + '@babel/preset-env': + specifier: ^7.24.8 + version: 7.25.3(@babel/core@7.25.2) + '@babel/preset-typescript': + specifier: ^7.24.7 + version: 7.24.7(@babel/core@7.25.2) + '@mdit/plugin-dl': + specifier: ^0.13.0 + version: 0.13.0(markdown-it@14.1.0) + '@types/fs-extra': + specifier: ^11.0.4 + version: 11.0.4 + '@types/markdown-it': + specifier: ^14.1.1 + version: 14.1.2 + '@vuepress/plugin-search': + specifier: ^2.0.0-rc.74 + version: 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + degit: + specifier: 2.8.4 + version: 2.8.4 + del: + specifier: 5.1.0 + version: 5.1.0 + dotenv: + specifier: 10.0.0 + version: 10.0.0 + eslint: + specifier: ^8.57.0 + version: 8.57.0 + eslint-config-vuepress: + specifier: ^4.10.1 + version: 4.10.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0) + eslint-config-vuepress-typescript: + specifier: ^4.10.1 + version: 4.10.1(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint-plugin-n@16.6.2(eslint@8.57.0))(eslint-plugin-promise@6.4.0(eslint@8.57.0))(eslint@8.57.0)(typescript@5.5.3) + fs-extra: + specifier: ^11.2.0 + version: 11.2.0 + markdown-it: + specifier: ^14.1.0 + version: 14.1.0 + prettier: + specifier: 2.3.2 + version: 2.3.2 + sass: + specifier: ^1.84.0 + version: 1.84.0 + shx: + specifier: 0.3.3 + version: 0.3.3 + stylus: + specifier: ^0.56.0 + version: 0.56.0 + tsconfig-vuepress: + specifier: ^4.5.0 + version: 4.5.0 + typescript: + specifier: ^5.5.3 + version: 5.5.3 + vite-plugin-vue-devtools: + specifier: ^7.3.6 + version: 7.3.8(rollup@4.34.6)(vite@6.0.11(sass@1.84.0)(stylus@0.56.0))(vue@3.4.31(typescript@5.5.3)) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@antfu/install-pkg@0.4.1': + resolution: {integrity: sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==} + + '@antfu/utils@0.7.10': + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + + '@babel/cli@7.24.8': + resolution: {integrity: sha512-isdp+G6DpRyKc+3Gqxy2rjzgF7Zj9K0mzLNnxz+E/fgeag8qT3vVulX4gY9dGO1q0y+0lUv6V3a+uhUzMzrwXg==} + engines: {node: '>=6.9.0'} + hasBin: true + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/code-frame@7.24.7': + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.25.2': + resolution: {integrity: sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.25.2': + resolution: {integrity: sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.25.0': + resolution: {integrity: sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.24.7': + resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': + resolution: {integrity: sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.2': + resolution: {integrity: sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.25.0': + resolution: {integrity: sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.25.2': + resolution: {integrity: sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.2': + resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-member-expression-to-functions@7.24.8': + resolution: {integrity: sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.22.15': + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.24.7': + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.25.2': + resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.24.7': + resolution: {integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.24.8': + resolution: {integrity: sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.25.0': + resolution: {integrity: sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.25.0': + resolution: {integrity: sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-simple-access@7.24.7': + resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-skip-transparent-expression-wrappers@7.24.7': + resolution: {integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.24.8': + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.24.8': + resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.25.0': + resolution: {integrity: sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.25.0': + resolution: {integrity: sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.24.7': + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.24.7': + resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.25.3': + resolution: {integrity: sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.3': + resolution: {integrity: sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.0': + resolution: {integrity: sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.0': + resolution: {integrity: sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7': + resolution: {integrity: sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.0': + resolution: {integrity: sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-decorators@7.24.7': + resolution: {integrity: sha512-RL9GR0pUG5Kc8BUWLNDm2T5OpYwSX15r98I0IkgmRQTXuELq/OynH8xtMTMvTJFjXbMWFVTKtYkTaYQsuAwQlQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.24.7': + resolution: {integrity: sha512-Ui4uLJJrRV1lb38zg1yYTmRKmiZLiftDEvZN2iq3kd9kUFU+PttmzTbAFC2ucRk/XJmtek6G23gPsuZbhrT8fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-dynamic-import@7.8.3': + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-export-namespace-from@7.8.3': + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.24.7': + resolution: {integrity: sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.24.7': + resolution: {integrity: sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.24.7': + resolution: {integrity: sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.24.7': + resolution: {integrity: sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.24.7': + resolution: {integrity: sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.25.0': + resolution: {integrity: sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.24.7': + resolution: {integrity: sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.24.7': + resolution: {integrity: sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.25.0': + resolution: {integrity: sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.24.7': + resolution: {integrity: sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.24.7': + resolution: {integrity: sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.25.0': + resolution: {integrity: sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.24.7': + resolution: {integrity: sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.24.8': + resolution: {integrity: sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.24.7': + resolution: {integrity: sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.24.7': + resolution: {integrity: sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.0': + resolution: {integrity: sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.24.7': + resolution: {integrity: sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.24.7': + resolution: {integrity: sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.24.7': + resolution: {integrity: sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.24.7': + resolution: {integrity: sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.25.1': + resolution: {integrity: sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.24.7': + resolution: {integrity: sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.25.2': + resolution: {integrity: sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.24.7': + resolution: {integrity: sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.24.7': + resolution: {integrity: sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.24.7': + resolution: {integrity: sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.24.8': + resolution: {integrity: sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.25.0': + resolution: {integrity: sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.24.7': + resolution: {integrity: sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.24.7': + resolution: {integrity: sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.24.7': + resolution: {integrity: sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.24.7': + resolution: {integrity: sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.24.7': + resolution: {integrity: sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.24.7': + resolution: {integrity: sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.24.7': + resolution: {integrity: sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.24.7': + resolution: {integrity: sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.24.8': + resolution: {integrity: sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.24.7': + resolution: {integrity: sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.24.7': + resolution: {integrity: sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.24.7': + resolution: {integrity: sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.24.7': + resolution: {integrity: sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.24.7': + resolution: {integrity: sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-reserved-words@7.24.7': + resolution: {integrity: sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.24.7': + resolution: {integrity: sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.24.7': + resolution: {integrity: sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.24.7': + resolution: {integrity: sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.24.7': + resolution: {integrity: sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.24.8': + resolution: {integrity: sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.25.2': + resolution: {integrity: sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.24.7': + resolution: {integrity: sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.24.7': + resolution: {integrity: sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.24.7': + resolution: {integrity: sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.24.7': + resolution: {integrity: sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.25.3': + resolution: {integrity: sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/preset-typescript@7.24.7': + resolution: {integrity: sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/regjsgen@0.8.0': + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} + + '@babel/runtime@7.25.0': + resolution: {integrity: sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.25.0': + resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.3': + resolution: {integrity: sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.24.8': + resolution: {integrity: sha512-SkSBEHwwJRU52QEVZBmMBnE5Ux2/6WU1grdYyOhpbCNxbmJrDuDCphBzKZSO3taf0zztp+qkWlymE5tVL5l0TA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.25.2': + resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==} + engines: {node: '>=6.9.0'} + + '@braintree/sanitize-url@7.1.0': + resolution: {integrity: sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg==} + + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.24.2': + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.24.2': + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.24.2': + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.0': + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.11.0': + resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.0': + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.11.14': + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@2.1.33': + resolution: {integrity: sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@lit-labs/ssr-dom-shim@1.2.1': + resolution: {integrity: sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==} + + '@lit/reactive-element@2.0.4': + resolution: {integrity: sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==} + + '@mdit-vue/plugin-component@2.1.3': + resolution: {integrity: sha512-9AG17beCgpEw/4ldo/M6Y/1Rh4E1bqMmr/rCkWKmCAxy9tJz3lzY7HQJanyHMJufwsb3WL5Lp7Om/aPcQTZ9SA==} + + '@mdit-vue/plugin-frontmatter@2.1.3': + resolution: {integrity: sha512-KxsSCUVBEmn6sJcchSTiI5v9bWaoRxe68RBYRDGcSEY1GTnfQ5gQPMIsM48P4q1luLEIWurVGGrRu7u93//LDQ==} + + '@mdit-vue/plugin-headers@2.1.3': + resolution: {integrity: sha512-AcL7a7LHQR3ISINhfjGJNE/bHyM0dcl6MYm1Sr//zF7ZgokPGwD/HhD7TzwmrKA9YNYCcO9P3QmF/RN9XyA6CA==} + + '@mdit-vue/plugin-sfc@2.1.3': + resolution: {integrity: sha512-Ezl0dNvQNS639Yl4siXm+cnWtQvlqHrg+u+lnau/OHpj9Xh3LVap/BSQVugKIV37eR13jXXYf3VaAOP1fXPN+w==} + + '@mdit-vue/plugin-title@2.1.3': + resolution: {integrity: sha512-XWVOQoZqczoN97xCDrnQicmXKoqwOjIymIm9HQnRXhHnYKOgJPW1CxSGhkcOGzvDU1v0mD/adojVyyj/s6ggWw==} + + '@mdit-vue/plugin-toc@2.1.3': + resolution: {integrity: sha512-41Q+iXpLHZt0zJdApVwoVt7WF6za/xUjtjEPf90Z3KLzQO01TXsv48Xp9BsrFHPcPcm8tiZ0+O1/ICJO80V/MQ==} + + '@mdit-vue/shared@2.1.3': + resolution: {integrity: sha512-27YI8b0VVZsAlNwaWoaOCWbr4eL8B04HxiYk/y2ktblO/nMcOEOLt4p0RjuobvdyUyjHvGOS09RKhq7qHm1CHQ==} + + '@mdit-vue/types@2.1.0': + resolution: {integrity: sha512-TMBB/BQWVvwtpBdWD75rkZx4ZphQ6MN0O4QB2Bc0oI5PC2uE57QerhNxdRZ7cvBHE2iY2C+BUNUziCfJbjIRRA==} + + '@mdit/helper@0.16.0': + resolution: {integrity: sha512-vUmLSZp+7UXJIYxOya9BkD0OgjgQ+6gpX+htEnc4SKaDPx4S1E7h5TE6Wy4E9Gm/JhkMHoD6TdeoQwrN/I9cLQ==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-alert@0.16.0': + resolution: {integrity: sha512-T+0BUVhKjp+Azp6sNdDbiZwydDIcZP6/NAg9uivPvcsDnI9u4lMRCdXI090xNJOdhHO3l/lOsoO//s+++MJNtA==} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-align@0.16.0': + resolution: {integrity: sha512-BJhOjX4Zobs+ZKEpDtxGrUCnppkFCTGIBLjXkCPmxeLf4Tsh7dqv5vVhbRueSOz/EIzc2RJzR0dlMLofsaCFeA==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-attrs@0.16.7': + resolution: {integrity: sha512-N0zqyuDUO+VeM+vpmVCjujxAbuvE9DhYJoMV9GzLhzmJAP431JMsBBs/sRrJG9tvZrVFZbUMuq4uZz2CHAdkxQ==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-container@0.16.0': + resolution: {integrity: sha512-NCsyEiOmoJvXSEVJSY6vaEcvbE11sciRSx5qXBvQQZxUYGYsB+ObYSFVZDFPezsEN35X3b07rurLx8P2Mi9DgQ==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-demo@0.16.0': + resolution: {integrity: sha512-EoSpHz8ViLk5HLBCSzQZGOa36JXGHM4q5zOJ0ppgZymxnzRr6vUo+GX022uLivxyNMW1+l30IiF+jbse+JtBGw==} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-dl@0.13.0': + resolution: {integrity: sha512-6IC+82oDrXqxQ0Tg6ifTw70HoFk5hI/e9qQgmH5PAu8igTSWLOCSV1RfQc3bYEBKoSZGYnrjs/PtgJHgRLSgZQ==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-figure@0.16.0': + resolution: {integrity: sha512-0lYZX3cCUNaygtQXXZH2fHXzmF7sMZ5Jbk5MXDxEDIk1Nkxj8ADo/SctvXN5exwyGpJyw8nTbm7CGgMqifDpmQ==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-footnote@0.16.0': + resolution: {integrity: sha512-vaJWhOsya7bYfplLlMHYBxGTbME0e46/eTVKBROemWtAf873DTkV4IhkAq7MzGqeYrw0L9gxQPgGDFphGfySMA==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + + '@mdit/plugin-icon@0.16.5': + resolution: {integrity: sha512-9T34gnNrjCMdqNLnC1oi+kZT1iCnwlHAtH3D7sjVkcP8Cw4GoDoAGy50oyryivDlczrKubOFtF05lYAfXZauuA==} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-img-lazyload@0.16.0': + resolution: {integrity: sha512-Ilf3e5SKG7hd+RAoYQalpjoz8LMCxCe3BBHFYerv8u4wLnKe/L0Gqc8kXSpR37flzv3Ncw/NMqmD4ZZ0QQnK9A==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-img-mark@0.16.0': + resolution: {integrity: sha512-BUYqQRWUxNKB0BbMb8SZtlTeDZNXxuJ9AuiuB54RIWlbx3iRlQkbQI3B/AxTT5/EbRMDhxOq0R8PumBuA1gNFA==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-img-size@0.16.0': + resolution: {integrity: sha512-4FBvIHYWT22bjU+kO1I00xLtnCi7aXdZ7QD3CJnK4Xl6gN8/WB9IkfqYnBPv8yDiaZrabduQo8Dh8Dm8hPOm2A==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-include@0.16.0': + resolution: {integrity: sha512-9ESwsc+/jYkS0hIzpWqMQ9bHgHG//35datnfp0KUOql/DSuLVhufPtNkKNe/SVNO/+AOBTTlRYzej9Jl7JjD7g==} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-katex-slim@0.16.7': + resolution: {integrity: sha512-pHaaz0WVhBepyKLRNtk1GyZU+kf3oQ7qWoESOoLBEOkOakZJuVVT4oNfrFgbfW2obW983q3z9l4kHjMRx/bn5g==} + engines: {node: '>= 18'} + peerDependencies: + katex: ^0.16.9 + markdown-it: ^14.1.0 + peerDependenciesMeta: + katex: + optional: true + markdown-it: + optional: true + + '@mdit/plugin-mark@0.16.0': + resolution: {integrity: sha512-VY8HhLaNw6iO6E1pSZr3bG6MzyxcAdQmQ+S0r/l87S0EKHCBrUJusaUjxa9aTVHiBcgGUjg9aumribGrWfuitA==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-mathjax-slim@0.16.0': + resolution: {integrity: sha512-bbo6HtNOFdNMGZH/pxc3X1vZOvOW1FF9RMiAW2pkmyk7sPnMziB8uwxm0Ra1RajEC/NDxJ3wcF7xynkLmS6PfA==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + mathjax-full: ^3.2.2 + peerDependenciesMeta: + markdown-it: + optional: true + mathjax-full: + optional: true + + '@mdit/plugin-plantuml@0.16.0': + resolution: {integrity: sha512-ZjGOWYxPcGFq/TAJ2wOU6vCYH82685ERFQAC+xUsd/f6G41oGmk5i2aNqfNYYPmoQvcPvimGUPky9L6k2IXKXw==} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-spoiler@0.16.0': + resolution: {integrity: sha512-lm2lLx5H6649igzmbEe7KGsYfS6EOHn3Ps1ZdOHIFo0AY9eEh//gbjPOuJNJU58vtMnzLYzQHQKp/JqViYTIQQ==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-stylize@0.16.0': + resolution: {integrity: sha512-uxM9aFdgS5YCXOSNSdYyC+uXyCnmqv1VUPRNAv0g/iOts0pUp63ZEUEO2sNlbXj1rGGEWylXyXqh3OU9rRngzg==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-sub@0.16.0': + resolution: {integrity: sha512-XpGcZW11SAWuiWtx9aYugM67OLtQJSfN87Q/aZbEfm6ahgdbO5lAe/vBFTBmL9aDc2EVatytGeZL3kA7pfHlOA==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-sup@0.16.0': + resolution: {integrity: sha512-45Sws9TC9h9ZRB/IcXAae+uYXb+FkVr/rkr9eMYKMFKksjMBddN+WY3Gpl9O7LhaGPipqTkm68QZnRSS1jvFkw==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-tab@0.16.0': + resolution: {integrity: sha512-c+/oT319DIWaMHyx5chueW8cy4pjC7E09QOg3qp86abTCdG2ljGLOlMAQbst5i/iH684QG/i8EJpB4oUeQdhkw==} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-tasklist@0.16.0': + resolution: {integrity: sha512-pxVxartDd8LYxhdYxyrh4c7JEAq+4cEMLI1HNCHTMK9cfO+SoVd/YpibfrDUg+LHvffc8Pf2Yc8pWXNoW34B1g==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-tex@0.16.0': + resolution: {integrity: sha512-VWb5rJYP0eBRRjYhcaRE3r8UQkUaBXzu0l42ck7DOp+MSPsgXfS+bmk8/tyHG6/X/Mig9H92Lh1jzTqp3f5yKg==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mdit/plugin-uml@0.16.0': + resolution: {integrity: sha512-BIsq6PpmRgoThtVR2j4BGiRGis6jrcxxqQW3RICacrG52Ps2RWEGwu7B/IvXs+KJZJLJsrKFQ2Pqaxttbjx3kw==} + engines: {node: '>= 18'} + peerDependencies: + markdown-it: ^14.1.0 + peerDependenciesMeta: + markdown-it: + optional: true + + '@mermaid-js/parser@0.3.0': + resolution: {integrity: sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA==} + + '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': + resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@pkgr/core@0.1.1': + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.25': + resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + + '@rollup/plugin-alias@3.1.9': + resolution: {integrity: sha512-QI5fsEvm9bDzt32k39wpOwZhVzRcL5ydcffUHMyLVaVaLeC70I8TJZ17F1z1eMoLu4E/UOcH9BWVkKpIKdrfiw==} + engines: {node: '>=8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + + '@rollup/pluginutils@5.1.0': + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.34.6': + resolution: {integrity: sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.34.6': + resolution: {integrity: sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.34.6': + resolution: {integrity: sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.34.6': + resolution: {integrity: sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.34.6': + resolution: {integrity: sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.34.6': + resolution: {integrity: sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.34.6': + resolution: {integrity: sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.34.6': + resolution: {integrity: sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.34.6': + resolution: {integrity: sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.34.6': + resolution: {integrity: sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.34.6': + resolution: {integrity: sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': + resolution: {integrity: sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.34.6': + resolution: {integrity: sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.34.6': + resolution: {integrity: sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.34.6': + resolution: {integrity: sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.34.6': + resolution: {integrity: sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.34.6': + resolution: {integrity: sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.34.6': + resolution: {integrity: sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.34.6': + resolution: {integrity: sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==} + cpu: [x64] + os: [win32] + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@shikijs/core@2.3.2': + resolution: {integrity: sha512-s7vyL3LzUKm3Qwf36zRWlavX9BQMZTIq9B1almM63M5xBuSldnsTHCmsXzoF/Kyw4k7Xgas7yAyJz9VR/vcP1A==} + + '@shikijs/engine-javascript@2.3.2': + resolution: {integrity: sha512-w3IEMu5HfL/OaJTsMbIfZ1HRPnWVYRANeDtmsdIIEgUOcLjzFJFQwlnkckGjKHekEzNqlMLbgB/twnfZ/EEAGg==} + + '@shikijs/engine-oniguruma@2.3.2': + resolution: {integrity: sha512-vikMY1TroyZXUHIXbMnvY/mjtOxMn+tavcfAeQPgWS9FHcgFSUoEtywF5B5sOLb9NXb8P2vb7odkh3nj15/00A==} + + '@shikijs/langs@2.3.2': + resolution: {integrity: sha512-UqI6bSxFzhexIJficZLKeB1L2Sc3xoNiAV0yHpfbg5meck93du+EKQtsGbBv66Ki53XZPhnR/kYkOr85elIuFw==} + + '@shikijs/themes@2.3.2': + resolution: {integrity: sha512-QAh7D/hhfYKHibkG2tti8vxNt3ekAH5EqkXJeJbTh7FGvTCWEI7BHqNCtMdjFvZ0vav5nvUgdvA7/HI7pfsB4w==} + + '@shikijs/transformers@2.3.2': + resolution: {integrity: sha512-2HDnJumw8A/9GecRpTgvfqSbPjEbJ4DPWq5J++OVP1gNMLvbV0MqFsP4canqRNM1LqB7VmWY45Stipb0ZIJ+0A==} + + '@shikijs/types@2.3.2': + resolution: {integrity: sha512-CBaMY+a3pepyC4SETi7+bSzO0f6hxEQJUUuS4uD7zppzjmrN4ZRtBqxaT+wOan26CR9eeJ5iBhc4qvWEwn7Eeg==} + + '@shikijs/vscode-textmate@10.0.1': + resolution: {integrity: sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==} + + '@sindresorhus/merge-streams@2.3.0': + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@stackblitz/sdk@1.11.0': + resolution: {integrity: sha512-DFQGANNkEZRzFk1/rDP6TcFdM82ycHE+zfl9C/M/jXlH68jiqHWHFMQURLELoD8koxvu/eW5uhg94NSAZlYrUQ==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + + '@types/glob@7.2.0': + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + + '@types/hash-sum@1.0.2': + resolution: {integrity: sha512-UP28RddqY8xcU0SCEp9YKutQICXpaAq9N8U2klqF5hegGha7KzTOL8EdhIIV3bOSGBzjEpN9bU/d+nNZBdJYVw==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it-emoji@3.0.1': + resolution: {integrity: sha512-cz1j8R35XivBqq9mwnsrP2fsz2yicLhB8+PDtuVkKOExwEdsVBNI+ROL3sbhtR5occRZ66vT0QnwFZCqdjf3pA==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/minimatch@5.1.2': + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + + '@types/ms@0.7.31': + resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} + + '@types/node@17.0.45': + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + + '@types/sax@1.2.7': + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + + '@typescript-eslint/eslint-plugin@7.16.0': + resolution: {integrity: sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@7.16.0': + resolution: {integrity: sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@7.16.0': + resolution: {integrity: sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/type-utils@7.16.0': + resolution: {integrity: sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@7.16.0': + resolution: {integrity: sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/typescript-estree@7.16.0': + resolution: {integrity: sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@7.16.0': + resolution: {integrity: sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/visitor-keys@7.16.0': + resolution: {integrity: sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + '@vitejs/plugin-vue@5.2.1': + resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vue/babel-helper-vue-transform-on@1.2.2': + resolution: {integrity: sha512-nOttamHUR3YzdEqdM/XXDyCSdxMA9VizUKoroLX6yTyRtggzQMHXcmwh8a7ZErcJttIBIc9s68a1B8GZ+Dmvsw==} + + '@vue/babel-plugin-jsx@1.2.2': + resolution: {integrity: sha512-nYTkZUVTu4nhP199UoORePsql0l+wj7v/oyQjtThUVhJl1U+6qHuoVhIvR3bf7eVKjbCK+Cs2AWd7mi9Mpz9rA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.2.2': + resolution: {integrity: sha512-EntyroPwNg5IPVdUJupqs0CFzuf6lUrVvCspmv2J1FITLeGnUCuoGNNk78dgCusxEiYj6RMkTJflGSxk5aIC4A==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.4.31': + resolution: {integrity: sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==} + + '@vue/compiler-core@3.4.38': + resolution: {integrity: sha512-8IQOTCWnLFqfHzOGm9+P8OPSEDukgg3Huc92qSG49if/xI2SAwLHQO2qaPQbjCWPBcQoO1WYfXfTACUrWV3c5A==} + + '@vue/compiler-core@3.5.13': + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + + '@vue/compiler-dom@3.4.31': + resolution: {integrity: sha512-wK424WMXsG1IGMyDGyLqB+TbmEBFM78hIsOJ9QwUVLGrcSk0ak6zYty7Pj8ftm7nEtdU/DGQxAXp0/lM/2cEpQ==} + + '@vue/compiler-dom@3.4.38': + resolution: {integrity: sha512-Osc/c7ABsHXTsETLgykcOwIxFktHfGSUDkb05V61rocEfsFDcjDLH/IHJSNJP+/Sv9KeN2Lx1V6McZzlSb9EhQ==} + + '@vue/compiler-dom@3.5.13': + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + + '@vue/compiler-sfc@3.4.31': + resolution: {integrity: sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ==} + + '@vue/compiler-sfc@3.4.38': + resolution: {integrity: sha512-s5QfZ+9PzPh3T5H4hsQDJtI8x7zdJaew/dCGgqZ2630XdzaZ3AD8xGZfBqpT8oaD/p2eedd+pL8tD5vvt5ZYJQ==} + + '@vue/compiler-sfc@3.5.13': + resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + + '@vue/compiler-ssr@3.4.31': + resolution: {integrity: sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA==} + + '@vue/compiler-ssr@3.4.38': + resolution: {integrity: sha512-YXznKFQ8dxYpAz9zLuVvfcXhc31FSPFDcqr0kyujbOwNhlmaNvL2QfIy+RZeJgSn5Fk54CWoEUeW+NVBAogGaw==} + + '@vue/compiler-ssr@3.5.13': + resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + + '@vue/devtools-api@6.6.3': + resolution: {integrity: sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.2': + resolution: {integrity: sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA==} + + '@vue/devtools-core@7.3.8': + resolution: {integrity: sha512-mEwsR7GMklWuPOBH/++DiJe0GWqQ0syDtWP0HhU8m9tebs5zQtujMXrgu+cgBAKquJAWnBz0PwNzBgBD2P+M9A==} + peerDependencies: + vue: ^3.0.0 + + '@vue/devtools-kit@7.3.8': + resolution: {integrity: sha512-HYy3MQP1nZ6GbE4vrgJ/UB+MvZnhYmEwCa/UafrEpdpwa+jNCkz1ZdUrC5I7LpkH1ShREEV2/pZlAQdBj+ncLQ==} + + '@vue/devtools-kit@7.7.2': + resolution: {integrity: sha512-CY0I1JH3Z8PECbn6k3TqM1Bk9ASWxeMtTCvZr7vb+CHi+X/QwQm5F1/fPagraamKMAHVfuuCbdcnNg1A4CYVWQ==} + + '@vue/devtools-shared@7.3.8': + resolution: {integrity: sha512-1NiJbn7Yp47nPDWhFZyEKpB2+5/+7JYv8IQnU0ccMrgslPR2dL7u1DIyI7mLqy4HN1ll36gQy0k8GqBYSFgZJw==} + + '@vue/devtools-shared@7.7.2': + resolution: {integrity: sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA==} + + '@vue/reactivity@3.4.31': + resolution: {integrity: sha512-VGkTani8SOoVkZNds1PfJ/T1SlAIOf8E58PGAhIOUDYPC4GAmFA2u/E14TDAFcf3vVDKunc4QqCe/SHr8xC65Q==} + + '@vue/reactivity@3.5.13': + resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} + + '@vue/runtime-core@3.4.31': + resolution: {integrity: sha512-LDkztxeUPazxG/p8c5JDDKPfkCDBkkiNLVNf7XZIUnJ+66GVGkP+TIh34+8LtPisZ+HMWl2zqhIw0xN5MwU1cw==} + + '@vue/runtime-core@3.5.13': + resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} + + '@vue/runtime-dom@3.4.31': + resolution: {integrity: sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw==} + + '@vue/runtime-dom@3.5.13': + resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} + + '@vue/server-renderer@3.4.31': + resolution: {integrity: sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA==} + peerDependencies: + vue: 3.4.31 + + '@vue/server-renderer@3.5.13': + resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} + peerDependencies: + vue: 3.5.13 + + '@vue/shared@3.4.31': + resolution: {integrity: sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA==} + + '@vue/shared@3.4.38': + resolution: {integrity: sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw==} + + '@vue/shared@3.5.13': + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + + '@vuelidate/core@2.0.3': + resolution: {integrity: sha512-AN6l7KF7+mEfyWG0doT96z+47ljwPpZfi9/JrNMkOGLFv27XVZvKzRLXlmDPQjPl/wOB1GNnHuc54jlCLRNqGA==} + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^2.0.0 || >=3.0.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + '@vuepress/bundler-vite@2.0.0-rc.19': + resolution: {integrity: sha512-Vn0wEVRcdAld+8NJeELSwrj5JEPObRn0xpRWtAau/UwVWHmMLo16RRkTvXdjSiwpDWeP/9ztC5buyTXVoeb7Dw==} + + '@vuepress/bundlerutils@2.0.0-rc.19': + resolution: {integrity: sha512-ln5htptK14OMJV3yeGRxAwYhSkVxrTwEHEaifeWrFvjuNxj2kLmkCl7MDdzr232jSOWwkCcmbOyafbxMsaRDkQ==} + + '@vuepress/cli@2.0.0-rc.19': + resolution: {integrity: sha512-QFicPNIj3RZAJbHoLbeYlPJsPchnQLGuw0n8xv0eeUi9ejEXO1huWA8sLoPbTGdiDW+PHr1MHnaVMkyUfwaKcQ==} + hasBin: true + + '@vuepress/client@2.0.0-rc.19': + resolution: {integrity: sha512-vUAU6n4qmtXqthxkb4LHq0D+VWSDenwBDf0jUs7RaBLuOVrbPtmH/hs4k1vLIlGdwC3Zs/G6tlB4UmuZiiwR8Q==} + + '@vuepress/core@2.0.0-rc.19': + resolution: {integrity: sha512-rvmBPMIWS2dey/2QjxZoO0OcrUU46NE3mSLk3oU7JOP0cG7xvRxf6U1OXiwYLC3fPO4g6XbHiKe6gihkmL6VDA==} + + '@vuepress/helper@2.0.0-rc.74': + resolution: {integrity: sha512-k0FjkM9TKggcWkyZwXj4cLUIF3FBJ5iZGnC+Ln4OJVGD7k3SvT7TL7IaCZoFBIXTlepZwytsIN7K5Lbmpx0GfQ==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/highlighter-helper@2.0.0-rc.71': + resolution: {integrity: sha512-Hi9ira4VmX1MuRcagbSIZ/hHtwB4Fduz/NfiFGmOYX68zWIsQ1e90Ntku8GeI2MEDWlFxGU8PY/7VcXwINjoXQ==} + peerDependencies: + '@vueuse/core': ^12.2.0 + vuepress: 2.0.0-rc.19 + peerDependenciesMeta: + '@vueuse/core': + optional: true + + '@vuepress/markdown@2.0.0-rc.19': + resolution: {integrity: sha512-6jgUXhpEK55PEEGtPhz7Hq/JqTbLU8n9w2D7emXiK2FYcbeKpjoRIbVRzmzB/dXeK3NzHChANu2IIqpOT6Ba1w==} + + '@vuepress/plugin-active-header-links@2.0.0-rc.74': + resolution: {integrity: sha512-ErXPpq52hKS0AubppT8HOqST5BBr2ibMK8LF2ctmoS7fZr8VlRysVn6jpLRGdDG+hBIHqbHsitBwMp5y1k99ag==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-back-to-top@2.0.0-rc.74': + resolution: {integrity: sha512-/r7pUarK67s3ZedfoUQ7JxcOcrSTxcSMiu6ozQW5vfe7s3d2WzIeaW/dsXPlmAdCEU0MZcb5RXRCNHBdZ9Zo2Q==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-blog@2.0.0-rc.74': + resolution: {integrity: sha512-CxVrkwLT3BHkwWaEyaaqeZ5YZ9kdqLaNjTidw9zMK0LAFCm62MrCUrhJnUeSEy233Gi3YYSiCV8hPRnUDMMa7A==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-catalog@2.0.0-rc.74': + resolution: {integrity: sha512-Oqz5BXVVdGfGlfTg+wxwUn5RFCPIVkLykBejMJy6E5oLeQC7Ofp9tg9KJze6nPoknXJWY78MEwRq0UQIC9oX9g==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-comment@2.0.0-rc.74': + resolution: {integrity: sha512-pBDe4Ua5UOme3C4FWW1ZCXiTMGjOL5YppOtwexmrPxFMZ3G5OpN2Eli5AHQjsl2lQADtDwciKuVFspTpNWbdFw==} + peerDependencies: + '@waline/client': ^3.5.0 + artalk: ^2.9.0 + twikoo: ^1.6.39 + vuepress: 2.0.0-rc.19 + peerDependenciesMeta: + '@waline/client': + optional: true + artalk: + optional: true + twikoo: + optional: true + + '@vuepress/plugin-copy-code@2.0.0-rc.74': + resolution: {integrity: sha512-flyUj8Xwj0G2jKMTtTrdJGpMS4By90kJGgEbxDTobV4t/98hpBBvEiL1AQ8oGIcQFHH6U+eNRPytde6/7NxKlw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-copyright@2.0.0-rc.74': + resolution: {integrity: sha512-T0KM753aiJsfXPgWSRIdKHit4CB9pNDCXcz1xRBMKRtI2WdajWPHpF7clrmgIwmZzvgNhxz5DtW80vDbyp0G0A==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-git@2.0.0-rc.68': + resolution: {integrity: sha512-k/tXBSIyQM26UrmDK/mN1/q6gw8PmF2uLyIaso+B39qCOFQKUBq4uJF2a0oYTq9tpjM5AHwwBpytPE5cdV/BPQ==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-icon@2.0.0-rc.74': + resolution: {integrity: sha512-JwodDsB5jQuoqFEW7cAWNyJG1GyiEaHiaLphIPjZZaO+QIt5wgRakhydd8VK/PqICzav/WO8LknN/i6OqhpUYw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-links-check@2.0.0-rc.74': + resolution: {integrity: sha512-/g+mosEv2iqbTVD7QpPIP0f0OGC8cQEO6VZgwxj25Swcnq0ndsuq0NOO+SIRasdYZe2xTZ94eNXcZEcKlCA9uw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-markdown-ext@2.0.0-rc.74': + resolution: {integrity: sha512-+SrSu95GKoGSCvIG1EAMctF7YKbbPAc3phbz0DuywuJjhEo7dC8T74tYGwj+AG8BFTVPFr3rMJTssdpHiUogNw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-markdown-hint@2.0.0-rc.74': + resolution: {integrity: sha512-1vC11eie+85XoIxQNWFgevpkYCcnc3DMi+x7WAc89+7yk0gP7zJVolWaPH1lLNfmoMxmpfms5ssEnUpr3vHMEQ==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-markdown-image@2.0.0-rc.74': + resolution: {integrity: sha512-x0j3FNBoexTRurgy0ycPnW8na4FR5pJC1n/vAInqu5w6U2O7PANr7tgKUz2r+XXfRpNh3j5JyLKWmJLvHdu8UA==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-markdown-include@2.0.0-rc.74': + resolution: {integrity: sha512-yLoWBhlOoWLQrMD30hFKquNt7IZvQiW45O5unztEy2F9bI1MicyBxUnbSMe8d+HNkuAaKxIeDqyt5pVFbqLb0w==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-markdown-math@2.0.0-rc.74': + resolution: {integrity: sha512-C0rG1A1I3fxrQ1VQtNepnoPuUnnjtNd9lFaW8WeEIKuLQVt2jWKjPXK6yMeu/0OwtyYYPZ5PJmofn0mFLbOZTw==} + peerDependencies: + katex: ^0.16.10 + mathjax-full: ^3.2.2 + vuepress: 2.0.0-rc.19 + peerDependenciesMeta: + katex: + optional: true + mathjax-full: + optional: true + + '@vuepress/plugin-markdown-stylize@2.0.0-rc.75': + resolution: {integrity: sha512-ratBXmz4TeOANsjyC4/F0K3kUe0YpFF8+OoPmX6GqqnVnk0UAM50BALYy+ca1R2imC0HRXSWp2jEmsKRCTt6OQ==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-markdown-tab@2.0.0-rc.74': + resolution: {integrity: sha512-LhsOEVDfOLpyjBKwx9ZsMbWD8NVQkHgjT+AbZMd2f+fnOaTw7cvWtJxTsg6yQZt2c0Wc3268WtaqxeuqaHTZ6w==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-notice@2.0.0-rc.74': + resolution: {integrity: sha512-6cVP7x5zDup+65tjkRnZhBy3tHXhN7pNcBcACKrLE/G3p1rb6SZiSoNk1H2Iva0RAFARzP1fztgTgasLWurofg==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-nprogress@2.0.0-rc.74': + resolution: {integrity: sha512-tgbMm2+MwJaUzqTBioeXYs8gaPXS9gYbvTg6HpFU0B4dJJ3CBq62CZEuord6T3Q6m/PnZz1H98bb3BmosKg1OA==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-photo-swipe@2.0.0-rc.74': + resolution: {integrity: sha512-yeFIXmlzefQOrKBFWKN4KYi8YM8rKRMD2M/L1hqtPp1rBudhfOva4c6ZKqgYnTyf7A1KlZRer1QCUQ3GWdRxew==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-reading-time@2.0.0-rc.74': + resolution: {integrity: sha512-VK7hwq077eiZ4igVLzX01Dioxy40DXqCSgNHtoycfrsQjqBuxyokVEQHe2+q0jvGLBXcrt381x/ZCDsUwVZhDw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-redirect@2.0.0-rc.74': + resolution: {integrity: sha512-zSfwcKD25MBGC2BZ/VD6XxJtpgDeUoAbZvkn3kuhakYllUHz94cFmxgbu+RukwL78Nam/UFj6ukyh4YCH3mMgg==} + hasBin: true + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-rtl@2.0.0-rc.74': + resolution: {integrity: sha512-T77zrw5htxe3uwjgqPKN1iU2zQlPnXl/YrQPVqIWEJJ4uNt93ZLyU892SiGWXNFMmqn8wes9PziCzBEAXqlKkQ==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-sass-palette@2.0.0-rc.74': + resolution: {integrity: sha512-bNXw/mMQrgRhhWGKx/W+agFORLbR33Z6FyNbGk6u7ZCSkuKlMcu8A5H+GLl4Jr0uvTTF6UESAxsJZVtRGZcTSQ==} + peerDependencies: + sass: ^1.80.3 + sass-embedded: ^1.80.3 + sass-loader: ^16.0.2 + vuepress: 2.0.0-rc.19 + peerDependenciesMeta: + sass: + optional: true + sass-embedded: + optional: true + sass-loader: + optional: true + + '@vuepress/plugin-search@2.0.0-rc.74': + resolution: {integrity: sha512-zDSl/vLRjtQIT8a8jZJ/fFQyJYZMSqcFJFUqpOWojNgDiTNZwa83PJxl0Tq8YdfSgmi4odwTl06xzpAuY6Pi3g==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-seo@2.0.0-rc.74': + resolution: {integrity: sha512-Z5Q35Y3TALhfhOs8DocBtQcyRCp0/Btjec7DfnDih5p5rhRI7dHI7DIdf9aJHTuz1VxpzCfru6sApqSdbPlc5g==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-shiki@2.0.0-rc.74': + resolution: {integrity: sha512-75wMcxa18JhFdTpGPzCeKJl0bc6gZ/ODKRbJo7wRRNLo3UOFBAcqTER3az2hi5b1xVUKrLWkbULSGivfeyvPSw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-sitemap@2.0.0-rc.74': + resolution: {integrity: sha512-Kbr9u3fryw34s9ZdxY4fKsCQcN74aFal34CJ4xPxx5E6liE9Rp+gOWevOl89qYXfXgPfyHHJlW5KYfonaZe9Sw==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/plugin-theme-data@2.0.0-rc.74': + resolution: {integrity: sha512-6uQPv4kRakqcEPWmL3ZYKqjXqzOVycAdlr7oQlxs23E8CO59/QyIcrkloHPsdI+VhAA3v46NdiVD2TIrESRm6A==} + peerDependencies: + vuepress: 2.0.0-rc.19 + + '@vuepress/shared@2.0.0-rc.19': + resolution: {integrity: sha512-xaDeZxX0Qetc2Y6/lrzO6M/40i3LmMm7Fk85bOftBBOaNehZ24RdsmIHBJDDv+bTUv+DBF++1/mOtbt6DBRzEA==} + + '@vuepress/utils@2.0.0-rc.19': + resolution: {integrity: sha512-cgzk8/aJquZKgFMNTuqdjbU5NrCzrPmdTyhYBcmliL/6N/He1OTWn3PD9QWUGJNODb1sPRJpklZnCpU07waLmg==} + + '@vueuse/core@12.5.0': + resolution: {integrity: sha512-GVyH1iYqNANwcahAx8JBm6awaNgvR/SwZ1fjr10b8l1HIgDp82ngNbfzJUgOgWEoxjL+URAggnlilAEXwCOZtg==} + + '@vueuse/metadata@12.5.0': + resolution: {integrity: sha512-Ui7Lo2a7AxrMAXRF+fAp9QsXuwTeeZ8fIB9wsLHqzq9MQk+2gMYE2IGJW48VMJ8ecvCB3z3GsGLKLbSasQ5Qlg==} + + '@vueuse/shared@12.5.0': + resolution: {integrity: sha512-vMpcL1lStUU6O+kdj6YdHDixh0odjPAUM15uJ9f7MY781jcYkIwFA4iv2EfoIPO6vBmvutI1HxxAwmf0cx5ISQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.2: + resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + + array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array.prototype.findlastindex@1.2.5: + resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + + atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + babel-plugin-polyfill-corejs2@0.4.11: + resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.10.6: + resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.2: + resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balloon-css@1.2.0: + resolution: {integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==} + + bcrypt-ts@5.0.3: + resolution: {integrity: sha512-2FcgD12xPbwCoe5i9/HK0jJ1xA1m+QfC1e6htG9Bl/hNOnLyaFmQSlqLKcfe3QdnoMPKpKEGFCbESBTg+SJNOw==} + engines: {node: '>=18'} + + binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + + birpc@0.2.17: + resolution: {integrity: sha512-+hkTxhot+dWsLpp3gia5AkVHIsKlZybNT5gIYiDlNzJrmYPcTM9k5/w2uaj3IPpd7LlEYpmCj4Jj1nC41VhDFg==} + + birpc@0.2.19: + resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.23.2: + resolution: {integrity: sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + browserslist@4.23.3: + resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + + builtins@5.1.0: + resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001641: + resolution: {integrity: sha512-Phv5thgl67bHYo1TtMY/MurjkHhV4EDaCosezRXgZ8jzA/Ub+wjxAvbGvjoFENStinwi5kCyOYV3mi5tOGykwA==} + + caniuse-lite@1.0.30001651: + resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.0.0: + resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} + engines: {node: '>=18.17'} + + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + connect-history-api-fallback@2.0.0: + resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} + engines: {node: '>=0.8'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + core-js-compat@3.38.0: + resolution: {integrity: sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A==} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + create-codepen@2.0.0: + resolution: {integrity: sha512-ehJ0Zw5RSV2G4+/azUb7vEZWRSA/K9cW7HDock1Y9ViDexkgSJUZJRcObdw/YAWeXKjreEQV9l/igNSsJ1yw5A==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + css@3.0.0: + resolution: {integrity: sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.30.3: + resolution: {integrity: sha512-HncJ9gGJbVtw7YXtIs3+6YAFSSiKsom0amWc33Z7QbylbY2JGMrA0yz4EwrdTScZxnwclXeEZHzO5pxoy0ZE4g==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.10: + resolution: {integrity: sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==} + + data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + + dayjs@1.11.12: + resolution: {integrity: sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.5: + resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + + default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + degit@2.8.4: + resolution: {integrity: sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng==} + engines: {node: '>=8.0.0'} + hasBin: true + + del@5.1.0: + resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} + engines: {node: '>=8'} + + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + dompurify@3.1.6: + resolution: {integrity: sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==} + + domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + + dotenv@10.0.0: + resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==} + engines: {node: '>=10'} + + electron-to-chromium@1.4.825: + resolution: {integrity: sha512-OCcF+LwdgFGcsYPYC5keEEFC2XT0gBhrYbeGzHCx7i9qRFbzO/AqTmc/C/1xNhJj+JA7rzlN7mpBuStshh96Cg==} + + electron-to-chromium@1.5.9: + resolution: {integrity: sha512-HfkT8ndXR0SEkU8gBQQM3rz035bpE/hxkZ1YIt4KJPEFES68HfIU6LzKukH0H794Lm83WJtkSAMfEToxCs15VA==} + + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + + emoji-regex@10.3.0: + resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encoding-sniffer@0.2.0: + resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + envinfo@7.14.0: + resolution: {integrity: sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==} + engines: {node: '>=4'} + hasBin: true + + error-stack-parser-es@0.1.5: + resolution: {integrity: sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==} + + es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + + es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-config-prettier@9.1.0: + resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-config-standard@17.1.0: + resolution: {integrity: sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: '^15.0.0 || ^16.0.0 ' + eslint-plugin-promise: ^6.0.0 + + eslint-config-vuepress-typescript@4.10.1: + resolution: {integrity: sha512-MD6Z0K/GViVIIRuCym2CfHb8XB2JJ3MXPN5gNe+Bv2lAXOyi5/IUDR+BjBjA/Hdo6xrLHQOM+I8gtQrxUuddjg==} + + eslint-config-vuepress@4.10.1: + resolution: {integrity: sha512-VunYtLhFdDxReGRCp+Lba86f18Hd/Srb+tOVbonYFITUNtpr8y85JgtVrXCS9c5+JE9PPx0JcxAFDxWDtoTH7g==} + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.8.1: + resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-es-x@7.8.0: + resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + + eslint-plugin-import@2.29.1: + resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-n@16.6.2: + resolution: {integrity: sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-promise@6.4.0: + resolution: {integrity: sha512-/KWWRaD3fGkVCZsdR0RU53PSthFmoHVhZl+y9+6DqeDLSikLdlUVpVEAmI6iCRR5QyOjBYBqHZV/bdv4DJ4Gtw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-vue@9.27.0: + resolution: {integrity: sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + execa@9.5.2: + resolution: {integrity: sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==} + engines: {node: ^18.19.0 || >=20.5.0} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.13.0: + resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + + fs-readdir-recursive@1.1.0: + resolution: {integrity: sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.2.0: + resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} + engines: {node: '>=18'} + + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.7.5: + resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==} + + giscus@1.6.0: + resolution: {integrity: sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@10.0.2: + resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globby@14.0.2: + resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} + engines: {node: '>=18'} + + gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + + has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hash-sum@2.0.0: + resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-to-html@9.0.4: + resolution: {integrity: sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + human-signals@8.0.0: + resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==} + engines: {node: '>=18.18.0'} + + iconify-icon@2.1.0: + resolution: {integrity: sha512-lto4XU3bwTQnb+D/CsJ4dWAo0aDe+uPMxEtxyOodw9l7R9QnJUUab3GCehlw2M8mDHdeUu/ufx8PvRQiJphhXg==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + + immutable@5.0.3: + resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + + is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + + is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + + is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.14.0: + resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + + is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-cwd@2.2.0: + resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} + engines: {node: '>=6'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + + is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.0.0: + resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} + engines: {node: '>=18'} + + is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + katex@0.16.11: + resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + langium@3.0.0: + resolution: {integrity: sha512-+Ez9EoiByeoTu/2BXmEaZ06iPNXM6thWJp02KfBO/raSMyCJ4jw7AkWWa+zBCTm0+Tw1Fj9FOxdqSskyN5nAwg==} + engines: {node: '>=16.0.0'} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.2: + resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} + engines: {node: '>=14'} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + lit-element@4.1.0: + resolution: {integrity: sha512-gSejRUQJuMQjV2Z59KAS/D4iElUhwKpIyJvZ9w+DIagIQjfJnhR20h2Q5ddpzXGS+fF0tMZ/xEYGMnKmaI/iww==} + + lit-html@3.2.0: + resolution: {integrity: sha512-pwT/HwoxqI9FggTrYVarkBKFN9MlTUpLrDHubTmW4SrkL3kkqW5gxwbxMMUnbbRHBC0WTZnYHcjDSCM559VyfA==} + + lit@3.2.1: + resolution: {integrity: sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==} + + local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + markdown-it-anchor@9.2.0: + resolution: {integrity: sha512-sa2ErMQ6kKOA4l31gLGYliFQrMKkqSO0ZJgGhDHKijPf0pNFM9vghjAh3gn26pS4JDRs7Iwa9S36gxm3vgZTzg==} + peerDependencies: + '@types/markdown-it': '*' + markdown-it: '*' + + markdown-it-emoji@3.0.0: + resolution: {integrity: sha512-+rUD93bXHubA4arpEZO3q80so0qgoFJEKRkRbjKX8RTdca89v2kfyF+xR3i2sQTwql9tpPZPOQN5B+PunspXRg==} + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + marked@13.0.3: + resolution: {integrity: sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==} + engines: {node: '>= 18'} + hasBin: true + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + mermaid@11.3.0: + resolution: {integrity: sha512-fFmf2gRXLtlGzug4wpIGN+rQdZ30M8IZEB1D3eZkXNqC7puhqeURBcD/9tbwXsqBO+A6Nzzo3MSSepmnw5xSeg==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.1: + resolution: {integrity: sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mlly@1.7.2: + resolution: {integrity: sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==} + + mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.0.9: + resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==} + engines: {node: ^18 || >=20} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-inspect@1.13.2: + resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + oniguruma-to-es@3.1.0: + resolution: {integrity: sha512-BJ3Jy22YlgejHSO7Fvmz1kKazlaPmRSUH+4adTDUS/dKQ4wLxI+gALZ8updbaux7/m7fIlpgOZ5fp/Inq5jUAw==} + + open@10.1.0: + resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@3.0.0: + resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.2: + resolution: {integrity: sha512-VgXbyrSNsml4eHWIvxxG/nTL4wgybMTXCV2Un/+yEc3aDKKU6nQBZjbeP3Pl3qm9Qg92X/1ng4ffvCeD/zwHgg==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parse5-htmlparser2-tree-adapter@7.0.0: + resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + path-type@5.0.0: + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + photoswipe@5.4.4: + resolution: {integrity: sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==} + engines: {node: '>= 0.12.0'} + + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pkg-types@1.2.1: + resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-selector-parser@6.1.1: + resolution: {integrity: sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.41: + resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.2: + resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@2.3.2: + resolution: {integrity: sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==} + engines: {node: '>=10.13.0'} + hasBin: true + + pretty-ms@9.0.0: + resolution: {integrity: sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==} + engines: {node: '>=18'} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.1: + resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} + engines: {node: '>= 14.18.0'} + + rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + + regenerate-unicode-properties@10.1.1: + resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + + regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + + regexpu-core@5.3.2: + resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} + engines: {node: '>=4'} + + regjsparser@0.9.1: + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + + rollup@4.34.6: + resolution: {integrity: sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + run-applescript@7.0.0: + resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + + safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass-loader@15.0.0: + resolution: {integrity: sha512-mbXAL7sI/fgt3skXR6xHxtKkaGyxRrGf7zrU4hLLWxBDJEcAe0QsoNy92qKttCb3zfMniTkU2kD9yakUKtW7vQ==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + sass: ^1.3.0 + sass-embedded: '*' + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + node-sass: + optional: true + sass: + optional: true + sass-embedded: + optional: true + webpack: + optional: true + + sass@1.84.0: + resolution: {integrity: sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==} + engines: {node: '>=14.0.0'} + hasBin: true + + sax@1.2.4: + resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.2: + resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + engines: {node: '>=10'} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + + shiki@2.3.2: + resolution: {integrity: sha512-UZhz/gsUz7DHFbQBOJP7eXqvKyYvMGramxQiSDc83M/7OkWm6OdVHAReEc3vMLh6L6TRhgL9dvhXz9XDkCDaaw==} + + shx@0.3.3: + resolution: {integrity: sha512-nZJ3HFWVoTSyyB+evEKjJ1STiixGztlqwKLTUNV5KqMWtGey9fTd4KU1gdZ1X9BV6215pswQ/Jew9NsuS/fNDA==} + engines: {node: '>=6'} + hasBin: true + + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + + sitemap@8.0.0: + resolution: {integrity: sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A==} + engines: {node: '>=14.0.0', npm: '>=6.0.0'} + hasBin: true + + slash@2.0.0: + resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} + engines: {node: '>=6'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-resolve@0.6.0: + resolution: {integrity: sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + stylis@4.3.4: + resolution: {integrity: sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==} + + stylus@0.56.0: + resolution: {integrity: sha512-Ev3fOb4bUElwWu4F9P9WjnnaSpc8XB9OFHSFZSKMFL1CE1oM+oFXWEgAqPmmZIyhBihuqIQlFsVTypiiS9RxeA==} + hasBin: true + + superjson@2.2.1: + resolution: {integrity: sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==} + engines: {node: '>=16'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + synckit@0.9.2: + resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} + engines: {node: ^14.18.0 || >=16.0.0} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + ts-api-utils@1.3.0: + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tsconfig-vuepress@4.5.0: + resolution: {integrity: sha512-edJbEJwTQayS4+q5mIPsVY9UaZfgzJezNX/eO86rRgIIDsJyVdcUahr7WgqQWdYI7MIcdDvMRlSn0+Veznmq5g==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + + typescript@5.5.3: + resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + + unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + + undici@6.21.1: + resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==} + engines: {node: '>=18.17'} + + unicode-canonical-property-names-ecmascript@2.0.0: + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.1.0: + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + universalify@2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + + upath@2.0.1: + resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} + engines: {node: '>=4'} + + update-browserslist-db@1.1.0: + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite-hot-client@0.2.3: + resolution: {integrity: sha512-rOGAV7rUlUHX89fP2p2v0A2WWvV3QMX2UYq0fRqsWSvFvev4atHWqjwGoKaZT1VTKyLGk533ecu3eyd0o59CAg==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 + + vite-plugin-inspect@0.8.5: + resolution: {integrity: sha512-JvTUqsP1JNDw0lMZ5Z/r5cSj81VK2B7884LO1DC3GMBhdcjcsAnJjdWq7bzQL01Xbh+v60d3lju3g+z7eAtNew==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^3.1.0 || ^4.0.0 || ^5.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + vite-plugin-vue-devtools@7.3.8: + resolution: {integrity: sha512-b5t4wxCb5g5cjh+odNpgnB7iX7gA6FJnKugFqX2/YZX9I4fvMjlj1bUnCKnvPlmwnFxClYgdmgZcCh2RyhZgvw==} + engines: {node: '>=v14.21.3'} + peerDependencies: + vite: ^3.1.0 || ^4.0.0-0 || ^5.0.0-0 + + vite-plugin-vue-inspector@5.1.3: + resolution: {integrity: sha512-pMrseXIDP1Gb38mOevY+BvtNGNqiqmqa2pKB99lnLsADQww9w9xMbAfT4GB6RUoaOkSPrtlXqpq2Fq+Dj2AgFg==} + peerDependencies: + vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 + + vite@6.0.11: + resolution: {integrity: sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + vue-demi@0.13.11: + resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-router@4.4.0: + resolution: {integrity: sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==} + peerDependencies: + vue: ^3.2.0 + + vue-router@4.5.0: + resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==} + peerDependencies: + vue: ^3.2.0 + + vue@3.4.31: + resolution: {integrity: sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + vue@3.5.13: + resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + vuepress-plugin-components@2.0.0-rc.71: + resolution: {integrity: sha512-0EN/RgVUpinQaoLwqozYEzxhI8xHT+oDv0B6bw4DI6pFSlUzDP2adE1rw9UYeZDbVMZ9CD1SxuCLvc0dGEIGbw==} + engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} + peerDependencies: + artplayer: ^5.0.0 + dashjs: 4.7.4 + hls.js: ^1.4.12 + mpegts.js: ^1.7.3 + sass: ^1.81.0 + sass-embedded: ^1.81.0 + sass-loader: ^16.0.2 + vidstack: ^1.12.9 + vuepress: 2.0.0-rc.19 + peerDependenciesMeta: + artplayer: + optional: true + dashjs: + optional: true + hls.js: + optional: true + mpegts.js: + optional: true + sass: + optional: true + sass-embedded: + optional: true + sass-loader: + optional: true + vidstack: + optional: true + + vuepress-plugin-md-enhance@2.0.0-rc.71: + resolution: {integrity: sha512-0w5PAXUE4z9hFcM7ig/BlBn874JzX+7B59TPUWrehyeVfdldPPGA5m1v0+30+9jGotANk5cwfhqtI0KT7Lrn3Q==} + engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} + peerDependencies: + '@vue/repl': ^4.1.1 + chart.js: ^4.0.0 + echarts: ^5.0.0 + flowchart.ts: ^3.0.0 + kotlin-playground: ^1.23.0 + markmap-lib: ^0.18.5 + markmap-toolbar: ^0.18.5 + markmap-view: ^0.18.5 + mermaid: ^11.2.0 + sandpack-vue3: ^3.0.0 + sass: ^1.81.0 + sass-embedded: ^1.81.0 + sass-loader: ^16.0.2 + vuepress: 2.0.0-rc.19 + peerDependenciesMeta: + '@vue/repl': + optional: true + chart.js: + optional: true + echarts: + optional: true + flowchart.ts: + optional: true + kotlin-playground: + optional: true + markmap-lib: + optional: true + markmap-toolbar: + optional: true + markmap-view: + optional: true + mermaid: + optional: true + sandpack-vue3: + optional: true + sass: + optional: true + sass-embedded: + optional: true + sass-loader: + optional: true + + vuepress-shared@2.0.0-rc.71: + resolution: {integrity: sha512-hdRZx4Qtr5uSVs8Tx61il8pXgeBpa5BnruEIFvcy/kjOUblqPjX/NwxThWPSHO4AjRAz4zQ8Gq9JcXh2c/m7Ow==} + engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} + peerDependencies: + vuepress: 2.0.0-rc.19 + + vuepress-theme-hope@2.0.0-rc.71: + resolution: {integrity: sha512-tZbdXIYiYZHYLIhNdxWTHnbjCKhJWTFFkCqC73jBqBr9gqYyuzfQ/fSPWh/xaqwe/v33AUzJ2QHKao86m78mjg==} + engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} + peerDependencies: + '@vuepress/plugin-docsearch': 2.0.0-rc.74 + '@vuepress/plugin-feed': 2.0.0-rc.74 + '@vuepress/plugin-prismjs': 2.0.0-rc.74 + '@vuepress/plugin-pwa': 2.0.0-rc.74 + '@vuepress/plugin-revealjs': 2.0.0-rc.74 + '@vuepress/plugin-search': 2.0.0-rc.74 + '@vuepress/plugin-slimsearch': 2.0.0-rc.74 + '@vuepress/plugin-watermark': 2.0.0-rc.74 + nodejs-jieba: ^0.2.1 + sass: ^1.81.0 + sass-embedded: ^1.81.0 + sass-loader: ^16.0.2 + vuepress: 2.0.0-rc.19 + peerDependenciesMeta: + '@vuepress/plugin-docsearch': + optional: true + '@vuepress/plugin-feed': + optional: true + '@vuepress/plugin-prismjs': + optional: true + '@vuepress/plugin-pwa': + optional: true + '@vuepress/plugin-revealjs': + optional: true + '@vuepress/plugin-search': + optional: true + '@vuepress/plugin-slimsearch': + optional: true + '@vuepress/plugin-watermark': + optional: true + nodejs-jieba: + optional: true + sass: + optional: true + sass-embedded: + optional: true + sass-loader: + optional: true + + vuepress@2.0.0-rc.19: + resolution: {integrity: sha512-JDeuPTu14Kprdqx2geAryjFJvUzVaMnOLewlAgwVuZTygDWb8cgXhu9/p6rqzzdHETtIrvjbASBhH7JPyqmxmA==} + engines: {node: ^18.19.0 || >=20.4.0} + hasBin: true + peerDependencies: + '@vuepress/bundler-vite': 2.0.0-rc.19 + '@vuepress/bundler-webpack': 2.0.0-rc.19 + vue: ^3.5.0 + peerDependenciesMeta: + '@vuepress/bundler-vite': + optional: true + '@vuepress/bundler-webpack': + optional: true + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@antfu/install-pkg@0.4.1': + dependencies: + package-manager-detector: 0.2.2 + tinyexec: 0.3.1 + + '@antfu/utils@0.7.10': {} + + '@babel/cli@7.24.8(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@jridgewell/trace-mapping': 0.3.25 + commander: 6.2.1 + convert-source-map: 2.0.0 + fs-readdir-recursive: 1.1.0 + glob: 7.2.3 + make-dir: 2.1.0 + slash: 2.0.0 + optionalDependencies: + '@nicolo-ribaudo/chokidar-2': 2.1.8-no-fsevents.3 + chokidar: 3.6.0 + + '@babel/code-frame@7.24.7': + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 + + '@babel/compat-data@7.25.2': {} + + '@babel/core@7.25.2': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.25.0 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helpers': 7.25.0 + '@babel/parser': 7.25.3 + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.3 + '@babel/types': 7.25.2 + convert-source-map: 2.0.0 + debug: 4.3.5 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.25.0': + dependencies: + '@babel/types': 7.25.2 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + + '@babel/helper-annotate-as-pure@7.24.7': + dependencies: + '@babel/types': 7.24.8 + + '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': + dependencies: + '@babel/traverse': 7.25.3 + '@babel/types': 7.24.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-compilation-targets@7.25.2': + dependencies: + '@babel/compat-data': 7.25.2 + '@babel/helper-validator-option': 7.24.8 + browserslist: 4.23.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-member-expression-to-functions': 7.24.8 + '@babel/helper-optimise-call-expression': 7.24.7 + '@babel/helper-replace-supers': 7.25.0(@babel/core@7.25.2) + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/traverse': 7.25.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.25.2(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-annotate-as-pure': 7.24.7 + regexpu-core: 5.3.2 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + debug: 4.3.5 + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-member-expression-to-functions@7.24.8': + dependencies: + '@babel/traverse': 7.25.3 + '@babel/types': 7.24.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.22.15': + dependencies: + '@babel/types': 7.24.8 + + '@babel/helper-module-imports@7.24.7': + dependencies: + '@babel/traverse': 7.25.3 + '@babel/types': 7.25.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.25.2(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + '@babel/traverse': 7.25.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.24.7': + dependencies: + '@babel/types': 7.24.8 + + '@babel/helper-plugin-utils@7.24.8': {} + + '@babel/helper-remap-async-to-generator@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-wrap-function': 7.25.0 + '@babel/traverse': 7.25.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-member-expression-to-functions': 7.24.8 + '@babel/helper-optimise-call-expression': 7.24.7 + '@babel/traverse': 7.25.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-simple-access@7.24.7': + dependencies: + '@babel/traverse': 7.25.3 + '@babel/types': 7.25.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.24.7': + dependencies: + '@babel/traverse': 7.25.3 + '@babel/types': 7.24.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.24.8': {} + + '@babel/helper-validator-identifier@7.24.7': {} + + '@babel/helper-validator-option@7.24.8': {} + + '@babel/helper-wrap-function@7.25.0': + dependencies: + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.3 + '@babel/types': 7.25.2 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.25.0': + dependencies: + '@babel/template': 7.25.0 + '@babel/types': 7.25.2 + + '@babel/highlight@7.24.7': + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 + + '@babel/parser@7.24.7': + dependencies: + '@babel/types': 7.24.8 + + '@babel/parser@7.25.3': + dependencies: + '@babel/types': 7.25.2 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/traverse': 7.25.3 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/plugin-transform-optional-chaining': 7.24.8(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/traverse': 7.25.3 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-decorators@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.25.0(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-decorators': 7.24.7(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-decorators@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-import-assertions@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-import-attributes@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-typescript@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-arrow-functions@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-async-generator-functions@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-remap-async-to-generator': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) + '@babel/traverse': 7.25.3 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-remap-async-to-generator': 7.25.0(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-block-scoping@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-class-properties@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.25.0(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.25.0(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-replace-supers': 7.25.0(@babel/core@7.25.2) + '@babel/traverse': 7.25.3 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/template': 7.25.0 + + '@babel/plugin-transform-destructuring@7.24.8(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-dotall-regex@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-duplicate-keys@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-dynamic-import@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) + + '@babel/plugin-transform-exponentiation-operator@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-export-namespace-from@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) + + '@babel/plugin-transform-for-of@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.25.1(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/traverse': 7.25.3 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) + + '@babel/plugin-transform-literals@7.25.2(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-logical-assignment-operators@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) + + '@babel/plugin-transform-member-expression-literals@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-modules-amd@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.24.8(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-simple-access': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.25.0(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + '@babel/traverse': 7.25.3 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-new-target@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-nullish-coalescing-operator@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) + + '@babel/plugin-transform-numeric-separator@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) + + '@babel/plugin-transform-object-rest-spread@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.25.2) + + '@babel/plugin-transform-object-super@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-replace-supers': 7.25.0(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) + + '@babel/plugin-transform-optional-chaining@7.24.8(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-private-methods@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.25.0(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.25.0(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-regenerator@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + regenerator-transform: 0.15.2 + + '@babel/plugin-transform-reserved-words@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-shorthand-properties@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-spread@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-template-literals@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-typeof-symbol@7.24.8(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-typescript@7.25.2(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.25.0(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-escapes@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-unicode-property-regex@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-unicode-regex@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/plugin-transform-unicode-sets-regex@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.25.2) + '@babel/helper-plugin-utils': 7.24.8 + + '@babel/preset-env@7.25.3(@babel/core@7.25.2)': + dependencies: + '@babel/compat-data': 7.25.2 + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-validator-option': 7.24.8 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.3(@babel/core@7.25.2) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.2) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-assertions': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-syntax-import-attributes': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.25.2) + '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-async-generator-functions': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-transform-async-to-generator': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-block-scoped-functions': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-block-scoping': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-transform-class-properties': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-class-static-block': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-classes': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-destructuring': 7.24.8(@babel/core@7.25.2) + '@babel/plugin-transform-dotall-regex': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-duplicate-keys': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-transform-dynamic-import': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-exponentiation-operator': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-export-namespace-from': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-for-of': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-function-name': 7.25.1(@babel/core@7.25.2) + '@babel/plugin-transform-json-strings': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-literals': 7.25.2(@babel/core@7.25.2) + '@babel/plugin-transform-logical-assignment-operators': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-member-expression-literals': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-modules-amd': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-modules-commonjs': 7.24.8(@babel/core@7.25.2) + '@babel/plugin-transform-modules-systemjs': 7.25.0(@babel/core@7.25.2) + '@babel/plugin-transform-modules-umd': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-named-capturing-groups-regex': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-new-target': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-nullish-coalescing-operator': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-numeric-separator': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-object-rest-spread': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-object-super': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-optional-catch-binding': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-optional-chaining': 7.24.8(@babel/core@7.25.2) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-private-methods': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-private-property-in-object': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-property-literals': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-regenerator': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-reserved-words': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-sticky-regex': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-typeof-symbol': 7.24.8(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-escapes': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-property-regex': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-regex': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-sets-regex': 7.24.7(@babel/core@7.25.2) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.25.2) + babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.25.2) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.25.2) + babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.25.2) + core-js-compat: 3.38.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/types': 7.24.8 + esutils: 2.0.3 + + '@babel/preset-typescript@7.24.7(@babel/core@7.25.2)': + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-validator-option': 7.24.8 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-modules-commonjs': 7.24.8(@babel/core@7.25.2) + '@babel/plugin-transform-typescript': 7.25.2(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + '@babel/regjsgen@0.8.0': {} + + '@babel/runtime@7.25.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.25.0': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.25.3 + '@babel/types': 7.25.2 + + '@babel/traverse@7.25.3': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.25.0 + '@babel/parser': 7.25.3 + '@babel/template': 7.25.0 + '@babel/types': 7.25.2 + debug: 4.3.5 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.24.8': + dependencies: + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + + '@babel/types@7.25.2': + dependencies: + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + + '@braintree/sanitize-url@7.1.0': {} + + '@chevrotain/cst-dts-gen@11.0.3': + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/gast@11.0.3': + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/regexp-to-ast@11.0.3': {} + + '@chevrotain/types@11.0.3': {} + + '@chevrotain/utils@11.0.3': {} + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.24.2': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.24.2': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.24.2': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.24.2': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.24.2': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.24.2': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.24.2': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.24.2': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.24.2': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.24.2': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.24.2': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.24.2': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.24.2': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.24.2': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.24.2': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.24.2': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.24.2': + optional: true + + '@esbuild/netbsd-arm64@0.24.2': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.24.2': + optional: true + + '@esbuild/openbsd-arm64@0.24.2': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.24.2': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.24.2': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.24.2': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.24.2': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.24.2': + optional: true + + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.11.0': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.3.5 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.0': {} + + '@humanwhocodes/config-array@0.11.14': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.3.5 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@iconify/types@2.0.0': {} + + '@iconify/utils@2.1.33': + dependencies: + '@antfu/install-pkg': 0.4.1 + '@antfu/utils': 0.7.10 + '@iconify/types': 2.0.0 + debug: 4.3.7 + kolorist: 1.8.0 + local-pkg: 0.5.0 + mlly: 1.7.2 + transitivePeerDependencies: + - supports-color + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@lit-labs/ssr-dom-shim@1.2.1': {} + + '@lit/reactive-element@2.0.4': + dependencies: + '@lit-labs/ssr-dom-shim': 1.2.1 + + '@mdit-vue/plugin-component@2.1.3': + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + + '@mdit-vue/plugin-frontmatter@2.1.3': + dependencies: + '@mdit-vue/types': 2.1.0 + '@types/markdown-it': 14.1.2 + gray-matter: 4.0.3 + markdown-it: 14.1.0 + + '@mdit-vue/plugin-headers@2.1.3': + dependencies: + '@mdit-vue/shared': 2.1.3 + '@mdit-vue/types': 2.1.0 + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + + '@mdit-vue/plugin-sfc@2.1.3': + dependencies: + '@mdit-vue/types': 2.1.0 + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + + '@mdit-vue/plugin-title@2.1.3': + dependencies: + '@mdit-vue/shared': 2.1.3 + '@mdit-vue/types': 2.1.0 + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + + '@mdit-vue/plugin-toc@2.1.3': + dependencies: + '@mdit-vue/shared': 2.1.3 + '@mdit-vue/types': 2.1.0 + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + + '@mdit-vue/shared@2.1.3': + dependencies: + '@mdit-vue/types': 2.1.0 + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + + '@mdit-vue/types@2.1.0': {} + + '@mdit/helper@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-alert@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-align@0.16.0(markdown-it@14.1.0)': + dependencies: + '@mdit/plugin-container': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-attrs@0.16.7(markdown-it@14.1.0)': + dependencies: + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-container@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-demo@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-dl@0.13.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-figure@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-footnote@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + + '@mdit/plugin-icon@0.16.5(markdown-it@14.1.0)': + dependencies: + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-img-lazyload@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-img-mark@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-img-size@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-include@0.16.0(markdown-it@14.1.0)': + dependencies: + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + upath: 2.0.1 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-katex-slim@0.16.7(katex@0.16.11)(markdown-it@14.1.0)': + dependencies: + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-tex': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + katex: 0.16.11 + markdown-it: 14.1.0 + + '@mdit/plugin-mark@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-mathjax-slim@0.16.0(markdown-it@14.1.0)': + dependencies: + '@mdit/plugin-tex': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + upath: 2.0.1 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-plantuml@0.16.0(markdown-it@14.1.0)': + dependencies: + '@mdit/plugin-uml': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-spoiler@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-stylize@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-sub@0.16.0(markdown-it@14.1.0)': + dependencies: + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-sup@0.16.0(markdown-it@14.1.0)': + dependencies: + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-tab@0.16.0(markdown-it@14.1.0)': + dependencies: + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-tasklist@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-tex@0.16.0(markdown-it@14.1.0)': + dependencies: + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mdit/plugin-uml@0.16.0(markdown-it@14.1.0)': + dependencies: + '@mdit/helper': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + optionalDependencies: + markdown-it: 14.1.0 + + '@mermaid-js/parser@0.3.0': + dependencies: + langium: 3.0.0 + + '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.13.0 + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@pkgr/core@0.1.1': {} + + '@polka/url@1.0.0-next.25': {} + + '@rollup/plugin-alias@3.1.9(rollup@4.34.6)': + dependencies: + rollup: 4.34.6 + slash: 3.0.0 + + '@rollup/pluginutils@5.1.0(rollup@4.34.6)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 2.3.1 + optionalDependencies: + rollup: 4.34.6 + + '@rollup/rollup-android-arm-eabi@4.34.6': + optional: true + + '@rollup/rollup-android-arm64@4.34.6': + optional: true + + '@rollup/rollup-darwin-arm64@4.34.6': + optional: true + + '@rollup/rollup-darwin-x64@4.34.6': + optional: true + + '@rollup/rollup-freebsd-arm64@4.34.6': + optional: true + + '@rollup/rollup-freebsd-x64@4.34.6': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.34.6': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.34.6': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.34.6': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-x64-musl@4.34.6': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.34.6': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.34.6': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.34.6': + optional: true + + '@sec-ant/readable-stream@0.4.1': {} + + '@shikijs/core@2.3.2': + dependencies: + '@shikijs/engine-javascript': 2.3.2 + '@shikijs/engine-oniguruma': 2.3.2 + '@shikijs/types': 2.3.2 + '@shikijs/vscode-textmate': 10.0.1 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.4 + + '@shikijs/engine-javascript@2.3.2': + dependencies: + '@shikijs/types': 2.3.2 + '@shikijs/vscode-textmate': 10.0.1 + oniguruma-to-es: 3.1.0 + + '@shikijs/engine-oniguruma@2.3.2': + dependencies: + '@shikijs/types': 2.3.2 + '@shikijs/vscode-textmate': 10.0.1 + + '@shikijs/langs@2.3.2': + dependencies: + '@shikijs/types': 2.3.2 + + '@shikijs/themes@2.3.2': + dependencies: + '@shikijs/types': 2.3.2 + + '@shikijs/transformers@2.3.2': + dependencies: + '@shikijs/core': 2.3.2 + '@shikijs/types': 2.3.2 + + '@shikijs/types@2.3.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.1 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.1': {} + + '@sindresorhus/merge-streams@2.3.0': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@stackblitz/sdk@1.11.0': {} + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 0.7.31 + + '@types/estree@1.0.6': {} + + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 17.0.45 + + '@types/glob@7.2.0': + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 17.0.45 + + '@types/hash-sum@1.0.2': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/json5@0.0.29': {} + + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 17.0.45 + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it-emoji@3.0.1': + dependencies: + '@types/markdown-it': 14.1.2 + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/minimatch@5.1.2': {} + + '@types/ms@0.7.31': {} + + '@types/node@17.0.45': {} + + '@types/sax@1.2.7': + dependencies: + '@types/node': 17.0.45 + + '@types/trusted-types@2.0.7': {} + + '@types/unist@3.0.3': {} + + '@types/web-bluetooth@0.0.20': {} + + '@typescript-eslint/eslint-plugin@7.16.0(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3)': + dependencies: + '@eslint-community/regexpp': 4.11.0 + '@typescript-eslint/parser': 7.16.0(eslint@8.57.0)(typescript@5.5.3) + '@typescript-eslint/scope-manager': 7.16.0 + '@typescript-eslint/type-utils': 7.16.0(eslint@8.57.0)(typescript@5.5.3) + '@typescript-eslint/utils': 7.16.0(eslint@8.57.0)(typescript@5.5.3) + '@typescript-eslint/visitor-keys': 7.16.0 + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.5.3) + optionalDependencies: + typescript: 5.5.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3)': + dependencies: + '@typescript-eslint/scope-manager': 7.16.0 + '@typescript-eslint/types': 7.16.0 + '@typescript-eslint/typescript-estree': 7.16.0(typescript@5.5.3) + '@typescript-eslint/visitor-keys': 7.16.0 + debug: 4.3.5 + eslint: 8.57.0 + optionalDependencies: + typescript: 5.5.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@7.16.0': + dependencies: + '@typescript-eslint/types': 7.16.0 + '@typescript-eslint/visitor-keys': 7.16.0 + + '@typescript-eslint/type-utils@7.16.0(eslint@8.57.0)(typescript@5.5.3)': + dependencies: + '@typescript-eslint/typescript-estree': 7.16.0(typescript@5.5.3) + '@typescript-eslint/utils': 7.16.0(eslint@8.57.0)(typescript@5.5.3) + debug: 4.3.5 + eslint: 8.57.0 + ts-api-utils: 1.3.0(typescript@5.5.3) + optionalDependencies: + typescript: 5.5.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@7.16.0': {} + + '@typescript-eslint/typescript-estree@7.16.0(typescript@5.5.3)': + dependencies: + '@typescript-eslint/types': 7.16.0 + '@typescript-eslint/visitor-keys': 7.16.0 + debug: 4.3.5 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.2 + ts-api-utils: 1.3.0(typescript@5.5.3) + optionalDependencies: + typescript: 5.5.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@7.16.0(eslint@8.57.0)(typescript@5.5.3)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@typescript-eslint/scope-manager': 7.16.0 + '@typescript-eslint/types': 7.16.0 + '@typescript-eslint/typescript-estree': 7.16.0(typescript@5.5.3) + eslint: 8.57.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@7.16.0': + dependencies: + '@typescript-eslint/types': 7.16.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.2.0': {} + + '@vitejs/plugin-vue@5.2.1(vite@6.0.11(sass@1.84.0)(stylus@0.56.0))(vue@3.5.13(typescript@5.5.3))': + dependencies: + vite: 6.0.11(sass@1.84.0)(stylus@0.56.0) + vue: 3.5.13(typescript@5.5.3) + + '@vue/babel-helper-vue-transform-on@1.2.2': {} + + '@vue/babel-plugin-jsx@1.2.2(@babel/core@7.25.2)': + dependencies: + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2) + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.3 + '@babel/types': 7.24.8 + '@vue/babel-helper-vue-transform-on': 1.2.2 + '@vue/babel-plugin-resolve-type': 1.2.2(@babel/core@7.25.2) + camelcase: 6.3.0 + html-tags: 3.3.1 + svg-tags: 1.0.0 + optionalDependencies: + '@babel/core': 7.25.2 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.2.2(@babel/core@7.25.2)': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/core': 7.25.2 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/parser': 7.24.7 + '@vue/compiler-sfc': 3.4.38 + + '@vue/compiler-core@3.4.31': + dependencies: + '@babel/parser': 7.24.7 + '@vue/shared': 3.4.31 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-core@3.4.38': + dependencies: + '@babel/parser': 7.24.7 + '@vue/shared': 3.4.38 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-core@3.5.13': + dependencies: + '@babel/parser': 7.25.3 + '@vue/shared': 3.5.13 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.4.31': + dependencies: + '@vue/compiler-core': 3.4.31 + '@vue/shared': 3.4.31 + + '@vue/compiler-dom@3.4.38': + dependencies: + '@vue/compiler-core': 3.4.38 + '@vue/shared': 3.4.38 + + '@vue/compiler-dom@3.5.13': + dependencies: + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-sfc@3.4.31': + dependencies: + '@babel/parser': 7.24.7 + '@vue/compiler-core': 3.4.31 + '@vue/compiler-dom': 3.4.31 + '@vue/compiler-ssr': 3.4.31 + '@vue/shared': 3.4.31 + estree-walker: 2.0.2 + magic-string: 0.30.10 + postcss: 8.4.41 + source-map-js: 1.2.0 + + '@vue/compiler-sfc@3.4.38': + dependencies: + '@babel/parser': 7.24.7 + '@vue/compiler-core': 3.4.38 + '@vue/compiler-dom': 3.4.38 + '@vue/compiler-ssr': 3.4.38 + '@vue/shared': 3.4.38 + estree-walker: 2.0.2 + magic-string: 0.30.10 + postcss: 8.4.41 + source-map-js: 1.2.1 + + '@vue/compiler-sfc@3.5.13': + dependencies: + '@babel/parser': 7.25.3 + '@vue/compiler-core': 3.5.13 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.2 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.4.31': + dependencies: + '@vue/compiler-dom': 3.4.31 + '@vue/shared': 3.4.31 + + '@vue/compiler-ssr@3.4.38': + dependencies: + '@vue/compiler-dom': 3.4.38 + '@vue/shared': 3.4.38 + + '@vue/compiler-ssr@3.5.13': + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/devtools-api@6.6.3': {} + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.2': + dependencies: + '@vue/devtools-kit': 7.7.2 + + '@vue/devtools-core@7.3.8(vite@6.0.11(sass@1.84.0)(stylus@0.56.0))(vue@3.4.31(typescript@5.5.3))': + dependencies: + '@vue/devtools-kit': 7.3.8 + '@vue/devtools-shared': 7.3.8 + mitt: 3.0.1 + nanoid: 3.3.7 + pathe: 1.1.2 + vite-hot-client: 0.2.3(vite@6.0.11(sass@1.84.0)(stylus@0.56.0)) + vue: 3.4.31(typescript@5.5.3) + transitivePeerDependencies: + - vite + + '@vue/devtools-kit@7.3.8': + dependencies: + '@vue/devtools-shared': 7.3.8 + birpc: 0.2.17 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.1 + + '@vue/devtools-kit@7.7.2': + dependencies: + '@vue/devtools-shared': 7.7.2 + birpc: 0.2.19 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.1 + + '@vue/devtools-shared@7.3.8': + dependencies: + rfdc: 1.4.1 + + '@vue/devtools-shared@7.7.2': + dependencies: + rfdc: 1.4.1 + + '@vue/reactivity@3.4.31': + dependencies: + '@vue/shared': 3.4.31 + + '@vue/reactivity@3.5.13': + dependencies: + '@vue/shared': 3.5.13 + + '@vue/runtime-core@3.4.31': + dependencies: + '@vue/reactivity': 3.4.31 + '@vue/shared': 3.4.31 + + '@vue/runtime-core@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/runtime-dom@3.4.31': + dependencies: + '@vue/reactivity': 3.4.31 + '@vue/runtime-core': 3.4.31 + '@vue/shared': 3.4.31 + csstype: 3.1.3 + + '@vue/runtime-dom@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/runtime-core': 3.5.13 + '@vue/shared': 3.5.13 + csstype: 3.1.3 + + '@vue/server-renderer@3.4.31(vue@3.4.31(typescript@5.5.3))': + dependencies: + '@vue/compiler-ssr': 3.4.31 + '@vue/shared': 3.4.31 + vue: 3.4.31(typescript@5.5.3) + + '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.5.3))': + dependencies: + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + vue: 3.5.13(typescript@5.5.3) + + '@vue/shared@3.4.31': {} + + '@vue/shared@3.4.38': {} + + '@vue/shared@3.5.13': {} + + '@vuelidate/core@2.0.3(vue@3.4.31(typescript@5.5.3))': + dependencies: + vue: 3.4.31(typescript@5.5.3) + vue-demi: 0.13.11(vue@3.4.31(typescript@5.5.3)) + + '@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3)': + dependencies: + '@vitejs/plugin-vue': 5.2.1(vite@6.0.11(sass@1.84.0)(stylus@0.56.0))(vue@3.5.13(typescript@5.5.3)) + '@vuepress/bundlerutils': 2.0.0-rc.19(typescript@5.5.3) + '@vuepress/client': 2.0.0-rc.19(typescript@5.5.3) + '@vuepress/core': 2.0.0-rc.19(typescript@5.5.3) + '@vuepress/shared': 2.0.0-rc.19 + '@vuepress/utils': 2.0.0-rc.19 + autoprefixer: 10.4.20(postcss@8.5.2) + connect-history-api-fallback: 2.0.0 + postcss: 8.5.2 + postcss-load-config: 6.0.1(postcss@8.5.2) + rollup: 4.34.6 + vite: 6.0.11(sass@1.84.0)(stylus@0.56.0) + vue: 3.5.13(typescript@5.5.3) + vue-router: 4.5.0(vue@3.5.13(typescript@5.5.3)) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - yaml + + '@vuepress/bundlerutils@2.0.0-rc.19(typescript@5.5.3)': + dependencies: + '@vuepress/client': 2.0.0-rc.19(typescript@5.5.3) + '@vuepress/core': 2.0.0-rc.19(typescript@5.5.3) + '@vuepress/shared': 2.0.0-rc.19 + '@vuepress/utils': 2.0.0-rc.19 + vue: 3.5.13(typescript@5.5.3) + vue-router: 4.5.0(vue@3.5.13(typescript@5.5.3)) + transitivePeerDependencies: + - supports-color + - typescript + + '@vuepress/cli@2.0.0-rc.19(typescript@5.5.3)': + dependencies: + '@vuepress/core': 2.0.0-rc.19(typescript@5.5.3) + '@vuepress/shared': 2.0.0-rc.19 + '@vuepress/utils': 2.0.0-rc.19 + cac: 6.7.14 + chokidar: 3.6.0 + envinfo: 7.14.0 + esbuild: 0.21.5 + transitivePeerDependencies: + - supports-color + - typescript + + '@vuepress/client@2.0.0-rc.19(typescript@5.5.3)': + dependencies: + '@vue/devtools-api': 7.7.2 + '@vuepress/shared': 2.0.0-rc.19 + vue: 3.5.13(typescript@5.5.3) + vue-router: 4.5.0(vue@3.5.13(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/core@2.0.0-rc.19(typescript@5.5.3)': + dependencies: + '@vuepress/client': 2.0.0-rc.19(typescript@5.5.3) + '@vuepress/markdown': 2.0.0-rc.19 + '@vuepress/shared': 2.0.0-rc.19 + '@vuepress/utils': 2.0.0-rc.19 + vue: 3.5.13(typescript@5.5.3) + transitivePeerDependencies: + - supports-color + - typescript + + '@vuepress/helper@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vue/shared': 3.5.13 + '@vueuse/core': 12.5.0(typescript@5.5.3) + cheerio: 1.0.0 + fflate: 0.8.2 + gray-matter: 4.0.3 + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/highlighter-helper@2.0.0-rc.71(@vueuse/core@12.5.0(typescript@5.5.3))(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + optionalDependencies: + '@vueuse/core': 12.5.0(typescript@5.5.3) + + '@vuepress/markdown@2.0.0-rc.19': + dependencies: + '@mdit-vue/plugin-component': 2.1.3 + '@mdit-vue/plugin-frontmatter': 2.1.3 + '@mdit-vue/plugin-headers': 2.1.3 + '@mdit-vue/plugin-sfc': 2.1.3 + '@mdit-vue/plugin-title': 2.1.3 + '@mdit-vue/plugin-toc': 2.1.3 + '@mdit-vue/shared': 2.1.3 + '@mdit-vue/types': 2.1.0 + '@types/markdown-it': 14.1.2 + '@types/markdown-it-emoji': 3.0.1 + '@vuepress/shared': 2.0.0-rc.19 + '@vuepress/utils': 2.0.0-rc.19 + markdown-it: 14.1.0 + markdown-it-anchor: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.0) + markdown-it-emoji: 3.0.0 + mdurl: 2.0.0 + transitivePeerDependencies: + - supports-color + + '@vuepress/plugin-active-header-links@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vueuse/core': 12.5.0(typescript@5.5.3) + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-back-to-top@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-blog@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + chokidar: 3.6.0 + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-catalog@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-comment@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + giscus: 1.6.0 + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-copy-code@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-copyright@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-git@2.0.0-rc.68(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + execa: 9.5.2 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + + '@vuepress/plugin-icon@2.0.0-rc.74(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@mdit/plugin-icon': 0.16.5(markdown-it@14.1.0) + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - markdown-it + - typescript + + '@vuepress/plugin-links-check@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-markdown-ext@2.0.0-rc.74(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@mdit/plugin-container': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-footnote': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-tasklist': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + js-yaml: 4.1.0 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - markdown-it + - typescript + + '@vuepress/plugin-markdown-hint@2.0.0-rc.74(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@mdit/plugin-alert': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-container': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - markdown-it + - typescript + + '@vuepress/plugin-markdown-image@2.0.0-rc.74(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@mdit/plugin-figure': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-img-lazyload': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-img-mark': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-img-size': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - markdown-it + - typescript + + '@vuepress/plugin-markdown-include@2.0.0-rc.74(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@mdit/plugin-include': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - markdown-it + - typescript + + '@vuepress/plugin-markdown-math@2.0.0-rc.74(katex@0.16.11)(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@mdit/plugin-katex-slim': 0.16.7(katex@0.16.11)(markdown-it@14.1.0) + '@mdit/plugin-mathjax-slim': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + optionalDependencies: + katex: 0.16.11 + transitivePeerDependencies: + - markdown-it + - typescript + + '@vuepress/plugin-markdown-stylize@2.0.0-rc.75(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@mdit/plugin-align': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-attrs': 0.16.7(markdown-it@14.1.0) + '@mdit/plugin-mark': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-spoiler': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-stylize': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-sub': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-sup': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - markdown-it + - typescript + + '@vuepress/plugin-markdown-tab@2.0.0-rc.74(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@mdit/plugin-tab': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - markdown-it + - typescript + + '@vuepress/plugin-notice@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-nprogress@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-photo-swipe@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + photoswipe: 5.4.4 + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-reading-time@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-redirect@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + commander: 13.1.0 + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-rtl@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-sass-palette@2.0.0-rc.74(sass-loader@15.0.0(sass@1.84.0))(sass@1.84.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + chokidar: 4.0.3 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + optionalDependencies: + sass: 1.84.0 + sass-loader: 15.0.0(sass@1.84.0) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-search@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + chokidar: 3.6.0 + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-seo@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-shiki@2.0.0-rc.74(@vueuse/core@12.5.0(typescript@5.5.3))(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@shikijs/transformers': 2.3.2 + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/highlighter-helper': 2.0.0-rc.71(@vueuse/core@12.5.0(typescript@5.5.3))(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + nanoid: 5.0.9 + shiki: 2.3.2 + synckit: 0.9.2 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - '@vueuse/core' + - typescript + + '@vuepress/plugin-sitemap@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + sitemap: 8.0.0 + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/plugin-theme-data@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)))': + dependencies: + '@vue/devtools-api': 7.7.2 + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + '@vuepress/shared@2.0.0-rc.19': + dependencies: + '@mdit-vue/types': 2.1.0 + + '@vuepress/utils@2.0.0-rc.19': + dependencies: + '@types/debug': 4.1.12 + '@types/fs-extra': 11.0.4 + '@types/hash-sum': 1.0.2 + '@vuepress/shared': 2.0.0-rc.19 + debug: 4.4.0 + fs-extra: 11.2.0 + globby: 14.0.2 + hash-sum: 2.0.0 + ora: 8.2.0 + picocolors: 1.1.1 + upath: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@vueuse/core@12.5.0(typescript@5.5.3)': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 12.5.0 + '@vueuse/shared': 12.5.0(typescript@5.5.3) + vue: 3.5.13(typescript@5.5.3) + transitivePeerDependencies: + - typescript + + '@vueuse/metadata@12.5.0': {} + + '@vueuse/shared@12.5.0(typescript@5.5.3)': + dependencies: + vue: 3.5.13(typescript@5.5.3) + transitivePeerDependencies: + - typescript + + acorn-jsx@5.3.2(acorn@8.12.1): + dependencies: + acorn: 8.12.1 + + acorn@8.12.1: {} + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-regex@6.0.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.2: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + + array-includes@3.1.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + is-string: 1.0.7 + + array-union@2.1.0: {} + + array.prototype.findlastindex@1.2.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + + array.prototype.flat@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-shim-unscopables: 1.0.2 + + array.prototype.flatmap@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-shim-unscopables: 1.0.2 + + arraybuffer.prototype.slice@1.0.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + + atob@2.1.2: {} + + autoprefixer@10.4.20(postcss@8.5.2): + dependencies: + browserslist: 4.23.3 + caniuse-lite: 1.0.30001651 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.1 + postcss: 8.5.2 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.25.2): + dependencies: + '@babel/compat-data': 7.25.2 + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.2) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.25.2): + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.2) + core-js-compat: 3.38.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.25.2): + dependencies: + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.2) + transitivePeerDependencies: + - supports-color + + balanced-match@1.0.2: {} + + balloon-css@1.2.0: {} + + bcrypt-ts@5.0.3: {} + + binary-extensions@2.2.0: {} + + birpc@0.2.17: {} + + birpc@0.2.19: {} + + boolbase@1.0.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.23.2: + dependencies: + caniuse-lite: 1.0.30001641 + electron-to-chromium: 1.4.825 + node-releases: 2.0.14 + update-browserslist-db: 1.1.0(browserslist@4.23.2) + + browserslist@4.23.3: + dependencies: + caniuse-lite: 1.0.30001651 + electron-to-chromium: 1.5.9 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.3) + + builtin-modules@3.3.0: {} + + builtins@5.1.0: + dependencies: + semver: 7.6.2 + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.0.0 + + cac@6.7.14: {} + + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001641: {} + + caniuse-lite@1.0.30001651: {} + + ccount@2.0.1: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.3.0: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + + cheerio@1.0.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + encoding-sniffer: 0.2.0 + htmlparser2: 9.1.0 + parse5: 7.1.2 + parse5-htmlparser2-tree-adapter: 7.0.0 + parse5-parser-stream: 7.1.2 + undici: 6.21.1 + whatwg-mimetype: 4.0.0 + + chevrotain-allstar@0.3.1(chevrotain@11.0.3): + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.17.21 + + chevrotain@11.0.3: + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.2 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.1 + + clean-stack@2.2.0: {} + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + comma-separated-tokens@2.0.3: {} + + commander@13.1.0: {} + + commander@6.2.1: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + connect-history-api-fallback@2.0.0: {} + + convert-source-map@2.0.0: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + core-js-compat@3.38.0: + dependencies: + browserslist: 4.23.3 + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + create-codepen@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + + css-what@6.1.0: {} + + css@3.0.0: + dependencies: + inherits: 2.0.4 + source-map: 0.6.1 + source-map-resolve: 0.6.0 + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.30.3): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.30.3 + + cytoscape-fcose@2.2.0(cytoscape@3.30.3): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.30.3 + + cytoscape@3.30.3: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.0: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.10: + dependencies: + d3: 7.9.0 + lodash-es: 4.17.21 + + data-view-buffer@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-offset@1.0.0: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + dayjs@1.11.12: {} + + dayjs@1.11.13: {} + + debug@3.2.7: + dependencies: + ms: 2.1.2 + + debug@4.3.5: + dependencies: + ms: 2.1.2 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decode-uri-component@0.2.2: {} + + deep-is@0.1.4: {} + + default-browser-id@5.0.0: {} + + default-browser@5.2.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + define-lazy-prop@3.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + degit@2.8.4: {} + + del@5.1.0: + dependencies: + globby: 10.0.2 + graceful-fs: 4.2.11 + is-glob: 4.0.3 + is-path-cwd: 2.2.0 + is-path-inside: 3.0.3 + p-map: 3.0.0 + rimraf: 3.0.2 + slash: 3.0.0 + + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + + dequal@2.0.3: {} + + detect-libc@1.0.3: + optional: true + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dijkstrajs@1.0.3: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + dompurify@3.1.6: {} + + domutils@3.1.0: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dotenv@10.0.0: {} + + electron-to-chromium@1.4.825: {} + + electron-to-chromium@1.5.9: {} + + emoji-regex-xs@1.0.0: {} + + emoji-regex@10.3.0: {} + + emoji-regex@8.0.0: {} + + encoding-sniffer@0.2.0: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + + entities@4.5.0: {} + + envinfo@7.14.0: {} + + error-stack-parser-es@0.1.5: {} + + es-abstract@1.23.3: + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.2 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + + es-errors@1.3.0: {} + + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.0.3: + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.0.2: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.2.1: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.24.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 + + escalade@3.1.2: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + eslint-compat-utils@0.5.1(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + semver: 7.6.2 + + eslint-config-prettier@9.1.0(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + + eslint-config-standard@17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint-plugin-n@16.6.2(eslint@8.57.0))(eslint-plugin-promise@6.4.0(eslint@8.57.0))(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0) + eslint-plugin-n: 16.6.2(eslint@8.57.0) + eslint-plugin-promise: 6.4.0(eslint@8.57.0) + + eslint-config-vuepress-typescript@4.10.1(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint-plugin-n@16.6.2(eslint@8.57.0))(eslint-plugin-promise@6.4.0(eslint@8.57.0))(eslint@8.57.0)(typescript@5.5.3): + dependencies: + '@typescript-eslint/eslint-plugin': 7.16.0(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3) + '@typescript-eslint/parser': 7.16.0(eslint@8.57.0)(typescript@5.5.3) + eslint-config-standard: 17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint-plugin-n@16.6.2(eslint@8.57.0))(eslint-plugin-promise@6.4.0(eslint@8.57.0))(eslint@8.57.0) + eslint-config-vuepress: 4.10.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0) + eslint-plugin-vue: 9.27.0(eslint@8.57.0) + transitivePeerDependencies: + - eslint + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - eslint-plugin-import + - eslint-plugin-n + - eslint-plugin-promise + - supports-color + - typescript + + eslint-config-vuepress@4.10.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0): + dependencies: + eslint-config-prettier: 9.1.0(eslint@8.57.0) + eslint-config-standard: 17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint-plugin-n@16.6.2(eslint@8.57.0))(eslint-plugin-promise@6.4.0(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0) + eslint-plugin-n: 16.6.2(eslint@8.57.0) + eslint-plugin-promise: 6.4.0(eslint@8.57.0) + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.14.0 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 7.16.0(eslint@8.57.0)(typescript@5.5.3) + eslint: 8.57.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + + eslint-plugin-es-x@7.8.0(eslint@8.57.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.11.0 + eslint: 8.57.0 + eslint-compat-utils: 0.5.1(eslint@8.57.0) + + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0): + dependencies: + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.5 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.16.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) + hasown: 2.0.2 + is-core-module: 2.14.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.0 + semver: 6.3.1 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 7.16.0(eslint@8.57.0)(typescript@5.5.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-n@16.6.2(eslint@8.57.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + builtins: 5.1.0 + eslint: 8.57.0 + eslint-plugin-es-x: 7.8.0(eslint@8.57.0) + get-tsconfig: 4.7.5 + globals: 13.24.0 + ignore: 5.3.1 + is-builtin-module: 3.2.1 + is-core-module: 2.14.0 + minimatch: 3.1.2 + resolve: 1.22.8 + semver: 7.6.2 + + eslint-plugin-promise@6.4.0(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + + eslint-plugin-vue@9.27.0(eslint@8.57.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + eslint: 8.57.0 + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.1 + semver: 7.6.2 + vue-eslint-parser: 9.4.3(eslint@8.57.0) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.11.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.3.5 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 3.4.3 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + execa@9.5.2: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.0 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.0.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.1 + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.13.0: + dependencies: + reusify: 1.0.4 + + fflate@0.8.2: {} + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.0.0 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.1: {} + + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + + fraction.js@4.3.7: {} + + fs-extra@11.2.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + + fs-readdir-recursive@1.1.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + functions-have-names: 1.2.3 + + functions-have-names@1.2.3: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.2.0: {} + + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + get-stream@8.0.1: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-symbol-description@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + + get-tsconfig@4.7.5: + dependencies: + resolve-pkg-maps: 1.0.0 + + giscus@1.6.0: + dependencies: + lit: 3.2.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@11.12.0: {} + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.0.1 + + globby@10.0.2: + dependencies: + '@types/glob': 7.2.0 + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + glob: 7.2.3 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 3.0.0 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 3.0.0 + + globby@14.0.2: + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.2 + ignore: 5.3.1 + path-type: 5.0.0 + slash: 5.1.0 + unicorn-magic: 0.1.0 + + gopd@1.0.1: + dependencies: + get-intrinsic: 1.2.4 + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + + hachure-fill@0.5.2: {} + + has-bigints@1.0.2: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 + + has-proto@1.0.3: {} + + has-symbols@1.0.3: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + + hash-sum@2.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-to-html@9.0.4: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hookable@5.5.3: {} + + html-tags@3.3.1: {} + + html-void-elements@3.0.0: {} + + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + + human-signals@5.0.0: {} + + human-signals@8.0.0: {} + + iconify-icon@2.1.0: + dependencies: + '@iconify/types': 2.0.0 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.1: {} + + immutable@5.0.3: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + internal-slot@1.0.7: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + interpret@1.4.0: {} + + is-array-buffer@3.0.4: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + + is-bigint@1.0.4: + dependencies: + has-bigints: 1.0.2 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.2.0 + + is-boolean-object@1.1.2: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-builtin-module@3.2.1: + dependencies: + builtin-modules: 3.3.0 + + is-callable@1.2.7: {} + + is-core-module@2.14.0: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.1: + dependencies: + is-typed-array: 1.1.13 + + is-date-object@1.0.5: + dependencies: + has-tostringtag: 1.0.2 + + is-docker@3.0.0: {} + + is-extendable@0.1.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@2.0.0: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-path-cwd@2.2.0: {} + + is-path-inside@3.0.3: {} + + is-plain-obj@4.1.0: {} + + is-regex@1.1.4: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-shared-array-buffer@1.0.3: + dependencies: + call-bind: 1.0.7 + + is-stream@3.0.0: {} + + is-stream@4.0.1: {} + + is-string@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-symbol@1.0.4: + dependencies: + has-symbols: 1.0.3 + + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.15 + + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.0.0: {} + + is-weakref@1.0.2: + dependencies: + call-bind: 1.0.7 + + is-what@4.1.16: {} + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@0.5.0: {} + + jsesc@2.5.2: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.11 + + katex@0.16.11: + dependencies: + commander: 8.3.0 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + khroma@2.1.0: {} + + kind-of@6.0.3: {} + + kolorist@1.8.0: {} + + langium@3.0.0: + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.2: {} + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + lit-element@4.1.0: + dependencies: + '@lit-labs/ssr-dom-shim': 1.2.1 + '@lit/reactive-element': 2.0.4 + lit-html: 3.2.0 + + lit-html@3.2.0: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.2.1: + dependencies: + '@lit/reactive-element': 2.0.4 + lit-element: 4.1.0 + lit-html: 3.2.0 + + local-pkg@0.5.0: + dependencies: + mlly: 1.7.2 + pkg-types: 1.2.1 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash.debounce@4.0.8: {} + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + log-symbols@6.0.0: + dependencies: + chalk: 5.3.0 + is-unicode-supported: 1.3.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.10: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + + markdown-it-anchor@9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.0): + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + + markdown-it-emoji@3.0.0: {} + + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + marked@13.0.3: {} + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdurl@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + mermaid@11.3.0: + dependencies: + '@braintree/sanitize-url': 7.1.0 + '@iconify/utils': 2.1.33 + '@mermaid-js/parser': 0.3.0 + cytoscape: 3.30.3 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.30.3) + cytoscape-fcose: 2.2.0(cytoscape@3.30.3) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.10 + dayjs: 1.11.12 + dompurify: 3.1.6 + katex: 0.16.11 + khroma: 2.1.0 + lodash-es: 4.17.21 + marked: 13.0.3 + roughjs: 4.6.6 + stylis: 4.3.4 + ts-dedent: 2.2.0 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-fn@4.0.0: {} + + mimic-function@5.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + mitt@3.0.1: {} + + mlly@1.7.2: + dependencies: + acorn: 8.12.1 + pathe: 1.1.2 + pkg-types: 1.2.1 + ufo: 1.5.4 + + mrmime@2.0.0: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + nanoid@3.3.7: {} + + nanoid@3.3.8: {} + + nanoid@5.0.9: {} + + natural-compare@1.4.0: {} + + neo-async@2.6.2: {} + + node-addon-api@7.1.1: + optional: true + + node-releases@2.0.14: {} + + node-releases@2.0.18: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-inspect@1.13.2: {} + + object-keys@1.1.1: {} + + object.assign@4.1.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + + object.values@1.2.0: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + oniguruma-to-es@3.1.0: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 6.0.1 + regex-recursion: 6.0.2 + + open@10.1.0: + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + is-wsl: 3.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@8.2.0: + dependencies: + chalk: 5.3.0 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.0.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@3.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-try@2.2.0: {} + + package-manager-detector@0.2.2: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-ms@4.0.0: {} + + parse5-htmlparser2-tree-adapter@7.0.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.1.2 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.1.2 + + parse5@7.1.2: + dependencies: + entities: 4.5.0 + + path-data-parser@0.1.0: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + path-type@5.0.0: {} + + pathe@1.1.2: {} + + perfect-debounce@1.0.0: {} + + photoswipe@5.4.4: {} + + picocolors@1.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + pify@4.0.1: {} + + pkg-types@1.2.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.2 + pathe: 1.1.2 + + pngjs@5.0.0: {} + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + possible-typed-array-names@1.0.0: {} + + postcss-load-config@6.0.1(postcss@8.5.2): + dependencies: + lilconfig: 3.1.2 + optionalDependencies: + postcss: 8.5.2 + + postcss-selector-parser@6.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.41: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.1 + + postcss@8.5.2: + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@2.3.2: {} + + pretty-ms@9.0.0: + dependencies: + parse-ms: 4.0.0 + + property-information@6.5.0: {} + + punycode.js@2.3.1: {} + + punycode@2.3.1: {} + + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + queue-microtask@1.2.3: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.1: {} + + rechoir@0.6.2: + dependencies: + resolve: 1.22.8 + + regenerate-unicode-properties@10.1.1: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regenerator-runtime@0.14.1: {} + + regenerator-transform@0.15.2: + dependencies: + '@babel/runtime': 7.25.0 + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.0.1: + dependencies: + regex-utilities: 2.3.0 + + regexp.prototype.flags@1.5.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + + regexpu-core@5.3.2: + dependencies: + '@babel/regjsgen': 0.8.0 + regenerate: 1.4.2 + regenerate-unicode-properties: 10.1.1 + regjsparser: 0.9.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.1.0 + + regjsparser@0.9.1: + dependencies: + jsesc: 0.5.0 + + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.14.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + reusify@1.0.4: {} + + rfdc@1.4.1: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + robust-predicates@3.0.2: {} + + rollup@4.34.6: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.34.6 + '@rollup/rollup-android-arm64': 4.34.6 + '@rollup/rollup-darwin-arm64': 4.34.6 + '@rollup/rollup-darwin-x64': 4.34.6 + '@rollup/rollup-freebsd-arm64': 4.34.6 + '@rollup/rollup-freebsd-x64': 4.34.6 + '@rollup/rollup-linux-arm-gnueabihf': 4.34.6 + '@rollup/rollup-linux-arm-musleabihf': 4.34.6 + '@rollup/rollup-linux-arm64-gnu': 4.34.6 + '@rollup/rollup-linux-arm64-musl': 4.34.6 + '@rollup/rollup-linux-loongarch64-gnu': 4.34.6 + '@rollup/rollup-linux-powerpc64le-gnu': 4.34.6 + '@rollup/rollup-linux-riscv64-gnu': 4.34.6 + '@rollup/rollup-linux-s390x-gnu': 4.34.6 + '@rollup/rollup-linux-x64-gnu': 4.34.6 + '@rollup/rollup-linux-x64-musl': 4.34.6 + '@rollup/rollup-win32-arm64-msvc': 4.34.6 + '@rollup/rollup-win32-ia32-msvc': 4.34.6 + '@rollup/rollup-win32-x64-msvc': 4.34.6 + fsevents: 2.3.3 + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + run-applescript@7.0.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rw@1.3.3: {} + + safe-array-concat@1.1.2: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + + safe-regex-test@1.0.3: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + + safer-buffer@2.1.2: {} + + sass-loader@15.0.0(sass@1.84.0): + dependencies: + neo-async: 2.6.2 + optionalDependencies: + sass: 1.84.0 + + sass@1.84.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.0.3 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + sax@1.2.4: {} + + sax@1.4.1: {} + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.6.2: {} + + set-blocking@2.0.0: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shelljs@0.8.5: + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + + shiki@2.3.2: + dependencies: + '@shikijs/core': 2.3.2 + '@shikijs/engine-javascript': 2.3.2 + '@shikijs/engine-oniguruma': 2.3.2 + '@shikijs/langs': 2.3.2 + '@shikijs/themes': 2.3.2 + '@shikijs/types': 2.3.2 + '@shikijs/vscode-textmate': 10.0.1 + '@types/hast': 3.0.4 + + shx@0.3.3: + dependencies: + minimist: 1.2.8 + shelljs: 0.8.5 + + side-channel@1.0.6: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.2 + + signal-exit@4.1.0: {} + + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.25 + mrmime: 2.0.0 + totalist: 3.0.1 + + sitemap@8.0.0: + dependencies: + '@types/node': 17.0.45 + '@types/sax': 1.2.7 + arg: 5.0.2 + sax: 1.4.1 + + slash@2.0.0: {} + + slash@3.0.0: {} + + slash@5.1.0: {} + + source-map-js@1.2.0: {} + + source-map-js@1.2.1: {} + + source-map-resolve@0.6.0: + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.2 + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + space-separated-tokens@2.0.2: {} + + speakingurl@14.0.1: {} + + sprintf-js@1.0.3: {} + + stdin-discarder@0.2.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.3.0 + get-east-asian-width: 1.2.0 + strip-ansi: 7.1.0 + + string.prototype.trim@1.2.9: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + + string.prototype.trimend@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.0.1 + + strip-bom-string@1.0.0: {} + + strip-bom@3.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-final-newline@4.0.0: {} + + strip-json-comments@3.1.1: {} + + stylis@4.3.4: {} + + stylus@0.56.0: + dependencies: + css: 3.0.0 + debug: 4.3.5 + glob: 7.2.3 + safer-buffer: 2.1.2 + sax: 1.2.4 + source-map: 0.7.4 + transitivePeerDependencies: + - supports-color + + superjson@2.2.1: + dependencies: + copy-anything: 3.0.5 + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svg-tags@1.0.0: {} + + synckit@0.9.2: + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.8.1 + + text-table@0.2.0: {} + + tinyexec@0.3.1: {} + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + trim-lines@3.0.1: {} + + ts-api-utils@1.3.0(typescript@5.5.3): + dependencies: + typescript: 5.5.3 + + ts-dedent@2.2.0: {} + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tsconfig-vuepress@4.5.0: {} + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typed-array-buffer@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + + typed-array-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-byte-offset@1.0.2: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-length@1.0.6: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + + typescript@5.5.3: {} + + uc.micro@2.1.0: {} + + ufo@1.5.4: {} + + unbox-primitive@1.0.2: + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + + undici@6.21.1: {} + + unicode-canonical-property-names-ecmascript@2.0.0: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-property-aliases-ecmascript: 2.1.0 + + unicode-match-property-value-ecmascript@2.1.0: {} + + unicode-property-aliases-ecmascript@2.1.0: {} + + unicorn-magic@0.1.0: {} + + unicorn-magic@0.3.0: {} + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + universalify@2.0.0: {} + + upath@2.0.1: {} + + update-browserslist-db@1.1.0(browserslist@4.23.2): + dependencies: + browserslist: 4.23.2 + escalade: 3.1.2 + picocolors: 1.0.1 + + update-browserslist-db@1.1.0(browserslist@4.23.3): + dependencies: + browserslist: 4.23.3 + escalade: 3.1.2 + picocolors: 1.0.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + uuid@9.0.1: {} + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + vite-hot-client@0.2.3(vite@6.0.11(sass@1.84.0)(stylus@0.56.0)): + dependencies: + vite: 6.0.11(sass@1.84.0)(stylus@0.56.0) + + vite-plugin-inspect@0.8.5(rollup@4.34.6)(vite@6.0.11(sass@1.84.0)(stylus@0.56.0)): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.1.0(rollup@4.34.6) + debug: 4.3.5 + error-stack-parser-es: 0.1.5 + fs-extra: 11.2.0 + open: 10.1.0 + perfect-debounce: 1.0.0 + picocolors: 1.0.1 + sirv: 2.0.4 + vite: 6.0.11(sass@1.84.0)(stylus@0.56.0) + transitivePeerDependencies: + - rollup + - supports-color + + vite-plugin-vue-devtools@7.3.8(rollup@4.34.6)(vite@6.0.11(sass@1.84.0)(stylus@0.56.0))(vue@3.4.31(typescript@5.5.3)): + dependencies: + '@vue/devtools-core': 7.3.8(vite@6.0.11(sass@1.84.0)(stylus@0.56.0))(vue@3.4.31(typescript@5.5.3)) + '@vue/devtools-kit': 7.3.8 + '@vue/devtools-shared': 7.3.8 + execa: 8.0.1 + sirv: 2.0.4 + vite: 6.0.11(sass@1.84.0)(stylus@0.56.0) + vite-plugin-inspect: 0.8.5(rollup@4.34.6)(vite@6.0.11(sass@1.84.0)(stylus@0.56.0)) + vite-plugin-vue-inspector: 5.1.3(vite@6.0.11(sass@1.84.0)(stylus@0.56.0)) + transitivePeerDependencies: + - '@nuxt/kit' + - rollup + - supports-color + - vue + + vite-plugin-vue-inspector@5.1.3(vite@6.0.11(sass@1.84.0)(stylus@0.56.0)): + dependencies: + '@babel/core': 7.25.2 + '@babel/plugin-proposal-decorators': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-syntax-import-attributes': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-transform-typescript': 7.25.2(@babel/core@7.25.2) + '@vue/babel-plugin-jsx': 1.2.2(@babel/core@7.25.2) + '@vue/compiler-dom': 3.4.38 + kolorist: 1.8.0 + magic-string: 0.30.10 + vite: 6.0.11(sass@1.84.0)(stylus@0.56.0) + transitivePeerDependencies: + - supports-color + + vite@6.0.11(sass@1.84.0)(stylus@0.56.0): + dependencies: + esbuild: 0.24.2 + postcss: 8.5.2 + rollup: 4.34.6 + optionalDependencies: + fsevents: 2.3.3 + sass: 1.84.0 + stylus: 0.56.0 + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.0.8: {} + + vue-demi@0.13.11(vue@3.4.31(typescript@5.5.3)): + dependencies: + vue: 3.4.31(typescript@5.5.3) + + vue-eslint-parser@9.4.3(eslint@8.57.0): + dependencies: + debug: 4.3.5 + eslint: 8.57.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + lodash: 4.17.21 + semver: 7.6.2 + transitivePeerDependencies: + - supports-color + + vue-router@4.4.0(vue@3.4.31(typescript@5.5.3)): + dependencies: + '@vue/devtools-api': 6.6.3 + vue: 3.4.31(typescript@5.5.3) + + vue-router@4.5.0(vue@3.5.13(typescript@5.5.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.13(typescript@5.5.3) + + vue@3.4.31(typescript@5.5.3): + dependencies: + '@vue/compiler-dom': 3.4.31 + '@vue/compiler-sfc': 3.4.31 + '@vue/runtime-dom': 3.4.31 + '@vue/server-renderer': 3.4.31(vue@3.4.31(typescript@5.5.3)) + '@vue/shared': 3.4.31 + optionalDependencies: + typescript: 5.5.3 + + vue@3.5.13(typescript@5.5.3): + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-sfc': 3.5.13 + '@vue/runtime-dom': 3.5.13 + '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.5.3)) + '@vue/shared': 3.5.13 + optionalDependencies: + typescript: 5.5.3 + + vuepress-plugin-components@2.0.0-rc.71(sass-loader@15.0.0(sass@1.84.0))(sass@1.84.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))): + dependencies: + '@stackblitz/sdk': 1.11.0 + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-sass-palette': 2.0.0-rc.74(sass-loader@15.0.0(sass@1.84.0))(sass@1.84.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + balloon-css: 1.2.0 + create-codepen: 2.0.0 + qrcode: 1.5.4 + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + vuepress-shared: 2.0.0-rc.71(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + optionalDependencies: + sass: 1.84.0 + sass-loader: 15.0.0(sass@1.84.0) + transitivePeerDependencies: + - typescript + + vuepress-plugin-md-enhance@2.0.0-rc.71(markdown-it@14.1.0)(mermaid@11.3.0)(sass-loader@15.0.0(sass@1.84.0))(sass@1.84.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))): + dependencies: + '@mdit/plugin-container': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-demo': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-plantuml': 0.16.0(markdown-it@14.1.0) + '@mdit/plugin-uml': 0.16.0(markdown-it@14.1.0) + '@types/markdown-it': 14.1.2 + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-sass-palette': 2.0.0-rc.74(sass-loader@15.0.0(sass@1.84.0))(sass@1.84.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + balloon-css: 1.2.0 + js-yaml: 4.1.0 + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + vuepress-shared: 2.0.0-rc.71(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + optionalDependencies: + mermaid: 11.3.0 + sass: 1.84.0 + sass-loader: 15.0.0(sass@1.84.0) + transitivePeerDependencies: + - markdown-it + - typescript + + vuepress-shared@2.0.0-rc.71(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))): + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + dayjs: 1.11.13 + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + transitivePeerDependencies: + - typescript + + vuepress-theme-hope@2.0.0-rc.71(@vuepress/plugin-search@2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))))(katex@0.16.11)(markdown-it@14.1.0)(mermaid@11.3.0)(sass-loader@15.0.0(sass@1.84.0))(sass@1.84.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))): + dependencies: + '@vuepress/helper': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-active-header-links': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-back-to-top': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-blog': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-catalog': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-comment': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-copy-code': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-copyright': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-git': 2.0.0-rc.68(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-icon': 2.0.0-rc.74(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-links-check': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-markdown-ext': 2.0.0-rc.74(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-markdown-hint': 2.0.0-rc.74(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-markdown-image': 2.0.0-rc.74(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-markdown-include': 2.0.0-rc.74(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-markdown-math': 2.0.0-rc.74(katex@0.16.11)(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-markdown-stylize': 2.0.0-rc.75(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-markdown-tab': 2.0.0-rc.74(markdown-it@14.1.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-notice': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-nprogress': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-photo-swipe': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-reading-time': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-redirect': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-rtl': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-sass-palette': 2.0.0-rc.74(sass-loader@15.0.0(sass@1.84.0))(sass@1.84.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-seo': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-shiki': 2.0.0-rc.74(@vueuse/core@12.5.0(typescript@5.5.3))(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-sitemap': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vuepress/plugin-theme-data': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + '@vueuse/core': 12.5.0(typescript@5.5.3) + balloon-css: 1.2.0 + bcrypt-ts: 5.0.3 + chokidar: 3.6.0 + vue: 3.5.13(typescript@5.5.3) + vuepress: 2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)) + vuepress-plugin-components: 2.0.0-rc.71(sass-loader@15.0.0(sass@1.84.0))(sass@1.84.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + vuepress-plugin-md-enhance: 2.0.0-rc.71(markdown-it@14.1.0)(mermaid@11.3.0)(sass-loader@15.0.0(sass@1.84.0))(sass@1.84.0)(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + vuepress-shared: 2.0.0-rc.71(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + optionalDependencies: + '@vuepress/plugin-search': 2.0.0-rc.74(typescript@5.5.3)(vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))) + sass: 1.84.0 + sass-loader: 15.0.0(sass@1.84.0) + transitivePeerDependencies: + - '@vue/repl' + - '@waline/client' + - artalk + - artplayer + - chart.js + - dashjs + - echarts + - flowchart.ts + - hls.js + - katex + - kotlin-playground + - markdown-it + - markmap-lib + - markmap-toolbar + - markmap-view + - mathjax-full + - mermaid + - mpegts.js + - sandpack-vue3 + - twikoo + - typescript + - vidstack + + vuepress@2.0.0-rc.19(@vuepress/bundler-vite@2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3)): + dependencies: + '@vuepress/cli': 2.0.0-rc.19(typescript@5.5.3) + '@vuepress/client': 2.0.0-rc.19(typescript@5.5.3) + '@vuepress/core': 2.0.0-rc.19(typescript@5.5.3) + '@vuepress/markdown': 2.0.0-rc.19 + '@vuepress/shared': 2.0.0-rc.19 + '@vuepress/utils': 2.0.0-rc.19 + vue: 3.4.31(typescript@5.5.3) + optionalDependencies: + '@vuepress/bundler-vite': 2.0.0-rc.19(sass@1.84.0)(stylus@0.56.0)(typescript@5.5.3) + transitivePeerDependencies: + - supports-color + - typescript + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + which-boxed-primitive@1.0.2: + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + + which-module@2.0.1: {} + + which-typed-array@1.1.15: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + xml-name-validator@4.0.0: {} + + y18n@4.0.3: {} + + yallist@3.1.1: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yocto-queue@0.1.0: {} + + yoctocolors@2.1.1: {} + + zwitch@2.0.4: {} diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 000000000..c9c18893e --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "tsconfig-vuepress/base.json", + "compilerOptions": { + "lib": ["DOM", "ES2022"], + "module": "ESNext", + "moduleResolution": "Bundler", + "target": "ES2022", + "declaration": false, + "allowJs": true + }, + "include": ["**/.vuepress/**/*"], + "exclude": ["node_modules", ".cache", ".temp", "dist"] +} \ No newline at end of file From 8ee8751edd641db26ce8d3123aee7c800c435f67 Mon Sep 17 00:00:00 2001 From: William Chong Date: Thu, 24 Jul 2025 13:16:56 +0400 Subject: [PATCH 02/29] Improve doc examples --- docs/api/appending-events.md | 136 ++++------- docs/api/authentication.md | 6 +- docs/api/delete-stream.md | 8 +- docs/api/getting-started.md | 62 ++--- docs/api/images/duplicate-event.png | Bin 26730 -> 0 bytes docs/api/observability.md | 64 ++--- docs/api/persistent-subscriptions.md | 26 +- docs/api/projections.md | 113 +++++---- docs/api/reading-events.md | 71 +++--- docs/api/subscriptions.md | 339 +++++++++------------------ 10 files changed, 329 insertions(+), 496 deletions(-) delete mode 100644 docs/api/images/duplicate-event.png diff --git a/docs/api/appending-events.md b/docs/api/appending-events.md index acbc089bc..35f220249 100644 --- a/docs/api/appending-events.md +++ b/docs/api/appending-events.md @@ -4,11 +4,9 @@ order: 2 # Appending events -When you start working with KurrentDB, it is empty. The first meaningful operation is to add one or more events to the database using one of the available client SDKs. - -::: tip -Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. -::: +When you start working with KurrentDB, your application streams are empty. The +first meaningful operation is to add one or more events to the database using +this API. ## Append your first event @@ -16,13 +14,11 @@ The simplest way to append an event to KurrentDB is to create an `EventData` obj ```cs var eventData = new EventData( - Uuid.NewUuid(), - "some-event", - "{\"id\": \"1\" \"value\": \"some value\"}"u8.ToArray() + Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( - "some-stream", + "order-123", StreamState.NoStream, new List { eventData @@ -42,57 +38,37 @@ If you are new to Event Sourcing, please study the [Handling concurrency](#handl Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **type**, and **data**. -### eventId +### EventId This takes the format of a `Uuid` and is used to uniquely identify the event you are trying to append. If two events with the same `Uuid` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```cs -var eventData = new EventData( - Uuid.NewUuid(), - "some-event", - "{\"id\": \"1\" \"value\": \"some value\"}"u8.ToArray() +var orderPlaced = new EventData( + Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); -await client.AppendToStreamAsync( - "same-event-stream", - StreamState.Any, - new List { - eventData - } -); +await client.AppendToStreamAsync("order-123", StreamState.Any, [orderPlaced]); // attempt to append the same event again -await client.AppendToStreamAsync( - "same-event-stream", - StreamState.Any, - new List { - eventData - } -); +await client.AppendToStreamAsync("order-123", StreamState.Any, [orderPlaced]); ``` -![Duplicate Event](./images/duplicate-event.png) - -### type +### Type Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. -### data +### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. -### metadata +### Metadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. -### isJson - -Simple boolean field to tell KurrentDB if the event is stored as json, true by default. - ## Handling concurrency When appending events to a stream, you can supply a *stream state* or *stream revision*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. @@ -100,34 +76,17 @@ When appending events to a stream, you can supply a *stream state* or *stream re For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```cs -var eventDataOne = new EventData( - Uuid.NewUuid(), - "some-event", - "{\"id\": \"1\" \"value\": \"some value\"}"u8.ToArray() -); +var orderPlaced = new EventData( + Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray()); -var eventDataTwo = new EventData( - Uuid.NewUuid(), - "some-event", - "{\"id\": \"2\" \"value\": \"some other value\"}"u8.ToArray() +var orderShipped = new EventData( + Uuid.NewUuid(), "OrderShipped", "{\"orderId\": \"123\"}"u8.ToArray() ); -await client.AppendToStreamAsync( - "no-stream-stream", - StreamState.NoStream, - new List { - eventDataOne - } -); +await client.AppendToStreamAsync("order-123", StreamState.NoStream, [orderPlaced]); -// attempt to append the same event again -await client.AppendToStreamAsync( - "no-stream-stream", - StreamState.NoStream, - new List { - eventDataTwo - } -); +// attempt to append the second event expecting no stream +await client.AppendToStreamAsync("order-123", StreamState.NoStream, [orderShipped]); ``` There are three available stream states: @@ -135,46 +94,34 @@ There are three available stream states: - `NoStream` - `StreamExists` -This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. - -```cs -var clientOneRead = client.ReadStreamAsync( - Direction.Forwards, - "concurrency-stream", - StreamPosition.Start -); - -var clientOneRevision = (await clientOneRead.LastAsync()).Event.EventNumber.ToUInt64(); +This check can be used to implement optimistic concurrency. When retrieving a +stream from KurrentDB, note the current version number. When you save it +back, you can determine if somebody else has modified the record in the +meantime. -var clientTwoRead = client.ReadStreamAsync(Direction.Forwards, "concurrency-stream", StreamPosition.Start); -var clientTwoRevision = (await clientTwoRead.LastAsync()).Event.EventNumber.ToUInt64(); +```cs{1-3,11,21} +var lastEvent = client + .ReadStreamAsync(Direction.Forwards, "order-123", StreamPosition.Start) + .LastAsync(); -var clientOneData = new EventData( - Uuid.NewUuid(), - "some-event", - "{\"id\": \"1\" \"value\": \"clientOne\"}"u8.ToArray() +var orderPaid = new EventData( + Uuid.NewUuid(), "OrderPaid", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( - "no-stream-stream", - clientOneRevision, - new List { - clientOneData - } + "order-123", + lastEvent.OriginalEventNumber.ToUInt64(), + [orderPaid] ); -var clientTwoData = new EventData( - Uuid.NewUuid(), - "some-event", - "{\"id\": \"2\" \"value\": \"clientTwo\"}"u8.ToArray() +var orderCancelled = new EventData( + Uuid.NewUuid(), "OrderCancelled", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( - "no-stream-stream", - clientTwoRevision, - new List { - clientTwoData - } + "order-123", + lastEvent.OriginalEventNumber.ToUInt64(), + [orderCancelled] ); ``` @@ -182,12 +129,11 @@ await client.AppendToStreamAsync( You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. -```cs +```cs{5} await client.AppendToStreamAsync( - "some-stream", + "order-123", StreamState.Any, new[] { eventData }, - userCredentials: new UserCredentials("admin", "changeit"), - cancellationToken: cancellationToken + userCredentials: new UserCredentials("admin", "changeit") ); ``` \ No newline at end of file diff --git a/docs/api/authentication.md b/docs/api/authentication.md index 3b432dfa8..75f87ddc9 100644 --- a/docs/api/authentication.md +++ b/docs/api/authentication.md @@ -1,10 +1,6 @@ --- title: Authentication order: 7 -head: - - - title - - {} - - Authentication | .NET | Clients | Kurrent Docs --- # Client x.509 certificate @@ -43,5 +39,5 @@ var settings = EventStoreClientSettings.Create( $"kurrentdb://localhost:2113/?tls=true&tlsVerifyCert=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}" ); -await using var client = new KurrentDBClient(settings); +await using var client = new EventStoreClient(settings); ``` \ No newline at end of file diff --git a/docs/api/delete-stream.md b/docs/api/delete-stream.md index 79daeb2e0..812766f63 100644 --- a/docs/api/delete-stream.md +++ b/docs/api/delete-stream.md @@ -1,9 +1,5 @@ --- order: 9 -head: - - - title - - {} - - Deleting Events | .NET | Clients | Kurrent Docs --- # Deleting Events @@ -16,7 +12,7 @@ When you need to fully remove a stream, KurrentDB offers two options: Soft Delet Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. -```csharp +```cs await client.DeleteAsync(streamName, StreamState.Any); ``` @@ -31,6 +27,6 @@ The stream can still be reopened by appending new events. Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. -```csharp +```cs await client.TombstoneAsync(streamName, StreamState.Any); ``` \ No newline at end of file diff --git a/docs/api/getting-started.md b/docs/api/getting-started.md index 509ba8ffc..21bcdbfe3 100644 --- a/docs/api/getting-started.md +++ b/docs/api/getting-started.md @@ -1,9 +1,5 @@ --- order: 1 -head: - - - title - - {} - - Getting Started | .NET | Clients | KurrentDB Docs --- # Getting started @@ -15,7 +11,10 @@ Get started by connecting your application to KurrentDB. To connect your application to KurrentDB, instantiate and configure the client. ::: tip Insecure clusters -All our GRPC clients are secure by default and must be configured to connect to an insecure server via [a connection string](#connection-string) or the client's configuration. +The recommended way to connect to KurrentDB is using secure mode (which is +the default). However, if your KurrentDB instance is running in insecure +mode, you must explicitly set `tls=false` in your connection +string or client configuration. ::: ### Required packages @@ -30,7 +29,7 @@ dotnet add package KurrentDB.Client --version "1.0.*" Each SDK has its own way of configuring the client, but the connection string can always be used. The KurrentDB connection string supports two schemas: `kurrentdb://` for connecting to a single-node server, and `kurrentdb+discover://` for connecting to a multi-node cluster. The difference between the two schemas is that when using `kurrentdb://`, the client will connect directly to the node; with `kurrentdb+discover://` schema the client will use the gossip protocol to retrieve the cluster information and choose the right node to connect to. -Since version 22.10, kurrentdb supports gossip on single-node deployments, so `kurrentdb+discover://` schema can be used for connecting to any topology. +Since version 22.10, ESDB supports gossip on single-node deployments, so `kurrentdb+discover://` schema can be used for connecting to any topology. The connection string has the following format: @@ -69,7 +68,7 @@ When connecting to an insecure instance, specify `tls=false` parameter. For exam First, create a client and get it connected to the database. ```cs -var client = new KurrentDBClient(KurrentDBClientSettings.Create("kurrentdb://admin:changeit@localhost:2113?tls=false&tlsVerifyCert=false")); +var client = new EventStoreClient(EventStoreClientSettings.Create("kurrentdb://admin:changeit@localhost:2113?tls=false&tlsVerifyCert=false")); ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. @@ -89,49 +88,50 @@ The code snippet below creates an event object instance, serializes it, and adds ```cs using System.Text.Json; -var evt = new TestEvent { - EntityId = Guid.NewGuid().ToString("N"), - ImportantData = "I wrote my first event!" +public class OrderCreated { + public string? OrderId { get; set; } +} + +var evt = new OrderCreated { + OrderId = Guid.NewGuid().ToString("N"), }; -var eventData = new EventData( - Uuid.NewUuid(), - "TestEvent", - JsonSerializer.SerializeToUtf8Bytes(evt) +var orderCreated = new EventData( + Uuid.NewUuid(), + "OrderCreated", + JsonSerializer.SerializeToUtf8Bytes(evt) ); ``` ### Appending events -Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. +Each event in the database has its own unique identifier (UUID). The database +uses it to ensure idempotent writes, but it only works if you specify the stream +revision when appending events to the stream. -In the snippet below, we append the event to the stream `some-stream`. +In the snippet below, we append the event to the stream `order-123`. ```cs -await client.AppendToStreamAsync( - "some-stream", - StreamState.Any, - new[] { eventData }, - cancellationToken: cancellationToken -); +await client.AppendToStreamAsync("order-123", StreamState.Any, [orderCreated]); ``` -Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). +Here we are appending events without checking if the stream exists or if the +stream version matches the expected event version. See more advanced scenarios +in [appending events documentation](./appending-events.md). ### Reading events -Finally, we can read events back from the `some-stream` stream. +Finally, we can read events back from the `order-123` stream. ```cs var result = client.ReadStreamAsync( - Direction.Forwards, - "some-stream", - StreamPosition.Start, - cancellationToken: cancellationToken + Direction.Forwards, "order-123", StreamPosition.Start ); -var events = await result.ToListAsync(cancellationToken); +var events = await result.ToListAsync(); ``` -When you read events from the stream, you get a collection of `ResolvedEvent` structures. The event payload is returned as a byte array and needs to be deserialized. See more advanced scenarios in [reading events documentation](./reading-events.md). - +When you read events from the stream, you get a collection of `ResolvedEvent` +structures. The event payload is returned as a byte array and needs to be +deserialized. See more advanced scenarios in +[reading events documentation](./reading-events.md). diff --git a/docs/api/images/duplicate-event.png b/docs/api/images/duplicate-event.png deleted file mode 100644 index 8f24748c564a42b47cab6a284555657bb00a60dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26730 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 S31)vFEXZ>;`C{33f&U9OBvs)6 diff --git a/docs/api/observability.md b/docs/api/observability.md index 146b22039..0a700cbdd 100644 --- a/docs/api/observability.md +++ b/docs/api/observability.md @@ -1,9 +1,5 @@ --- order: 8 -head: - - - title - - {} - - Observability | .NET | Clients | Kurrent Docs --- # Observability @@ -14,7 +10,13 @@ store operations with distributed tracing support. ## Prerequisites -You'll need to install exporters for your chosen observability platform: +Install the required OpenTelemetry package from NuGet: + +```bash +dotnet add package EventStore.Client.Extensions.OpenTelemetry +``` + +You'll also need to install exporters for your chosen observability platform: ```bash # For console output @@ -32,16 +34,17 @@ dotnet add package Seq.Extensions.Logging ## Basic Configuration -Configure instrumentation using the `AddKurrentDBClientInstrumentation()` +Configure instrumentation using the `AddEventStoreClientInstrumentation()` extension method. Here's a minimal setup: -```csharp {15} +```cs {15} +using EventStore.Client.Extensions.OpenTelemetry; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenTelemetry.Resources; using OpenTelemetry.Trace; -const string serviceName = "my-kurrentdb-app"; +const string serviceName = "my-eventstore-app"; var host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => @@ -49,7 +52,7 @@ var host = Host.CreateDefaultBuilder() services.AddOpenTelemetry() .ConfigureResource(builder => builder.AddService(serviceName)) .WithTracing(tracerBuilder => tracerBuilder - .AddKurrentDBClientInstrumentation() + .AddEventStoreClientInstrumentation() .AddConsoleExporter() ); }) @@ -66,16 +69,16 @@ observability platforms. You can find a list of available exporters in the You can configure multiple exporters simultaneously: -```csharp {10-18} +```cs {10-18} using OpenTelemetry.Exporter; var host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services.AddOpenTelemetry() - .ConfigureResource(builder => builder.AddService("my-kurrentdb-app")) + .ConfigureResource(builder => builder.AddService("my-eventstore-app")) .WithTracing(tracerBuilder => tracerBuilder - .AddKurrentDBClientInstrumentation() + .AddEventStoreClientInstrumentation() .AddConsoleExporter() .AddJaegerExporter(options => { @@ -104,19 +107,22 @@ subscription operations. Each trace includes metadata to help with debugging and monitoring: -| Attribute | Description | Example | -| ------------------------- | -------------------------------------- | ------------------------------------- | -| `db.user` | Database user name | `admin` | -| `db.system` | Database system identifier | `kurrentdb` | -| `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | -| `db.kurrentdb.stream` | Stream name or identifier | `user-events-123` | -| `server.address` | KurrentDB server address | `localhost` | -| `server.port` | KurrentDB server port | `2113` | -| `otel.status_code` | Status code for the operation | `UNSET`, `OK`, `ERROR` | -| `otel.status_description` | Status of a span | | -| `exception.type` | Exception type if an error occurred | | -| `exception.message` | Exception message if an error occurred | | -| `exception.stacktrace` | Stack trace of the exception | | +| Attribute | Description | Example | +| --------------------------------- | -------------------------------------- | ------------------------------------- | +| `db.user` | Database user name | `admin` | +| `db.system` | Database system identifier | `eventstoredb` | +| `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | +| `db.eventstoredb.stream` | Stream name or identifier | `user-events-123` | +| `db.eventstoredb.subscription.id` | Subscription identifier | `user-events-123-sub` | +| `db.eventstoredb.event.id` | Event identifier | `event-456` | +| `db.eventstoredb.event.type` | Event type identifier | `user.created` | +| `server.address` | KurrentDB server address | `localhost` | +| `server.port` | KurrentDB server port | `2113` | +| `otel.status_code` | Status code for the operation | `UNSET`, `OK`, `ERROR` | +| `otel.status_description` | Status of a span | | +| `exception.type` | Exception type if an error occurred | | +| `exception.message` | Exception message if an error occurred | | +| `exception.stacktrace` | Stack trace of the exception | | ### Sample Trace Output @@ -126,21 +132,21 @@ Here's an example trace from a stream append operation: Activity.TraceId: 8da04787239dbb85c1f9c6fba1b1f0d6 Activity.SpanId: 4352ec4a66a20b95 Activity.TraceFlags: Recorded -Activity.ActivitySourceName: kurrentdb +Activity.ActivitySourceName: eventstoredb Activity.DisplayName: streams.append Activity.Kind: Client Activity.StartTime: 2024-05-29T06:50:41.2519016Z Activity.Duration: 00:00:00.1500707 Activity.Tags: - db.kurrentdb.stream: example-stream + db.eventstoredb.stream: example-stream server.address: localhost server.port: 2113 - db.system: kurrentdb + db.system: eventstoredb db.operation: streams.append event.count: 3 StatusCode: Ok Resource associated with Activity: - service.name: my-kurrentdb-app + service.name: my-eventstore-app service.instance.id: 7316ef20-c354-4e64-97da-c1b99c2c28b0 service.version: 1.0.0 deployment.environment: production diff --git a/docs/api/persistent-subscriptions.md b/docs/api/persistent-subscriptions.md index e8e83ff4b..c7ff3f3df 100644 --- a/docs/api/persistent-subscriptions.md +++ b/docs/api/persistent-subscriptions.md @@ -1,9 +1,5 @@ --- order: 5 -head: - - - title - - {} - - Persistent Subscriptions | .NET | Clients | Kurrent Docs --- # Persistent Subscriptions @@ -16,6 +12,16 @@ Because of those, persistent subscriptions are defined as subscription groups th You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). +## Creating a client + +To work with persistent subscriptions, you need to create an instance of the `EventStorePersistentSubscriptionsClient`. This client is used to manage persistent subscriptions, including creating, updating, and deleting subscription groups. + +```cs +await using var client = new KurrentDBPersistentSubscriptionsClient( + KurrentDBClientSettings.Create("kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false") +); +``` + ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. @@ -26,7 +32,7 @@ The following examples demonstrate how to create a subscription group for a pers ```cs var settings = new PersistentSubscriptionSettings(); -await client.CreateToStreamAsync("example-stream", "subscription-group", settings); +await client.CreateToStreamAsync("order-123", "subscription-group", settings); ``` ### Subscribing to `$all` @@ -55,7 +61,7 @@ The code below shows how to connect to an existing subscription group for a spec ```cs await using var subscription = client.SubscribeToStream( - "example-stream", + "order-123", "subscription-group", cancellationToken: ct ); @@ -147,13 +153,13 @@ When creating a persistent subscription, you can choose between a number of cons ### RoundRobin (default) -Distributes events to all clients evenly. If the client `bufferSize` is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. +Distributes events to all clients evenly. If the buffer size is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle -Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process repeats. +Distributes events to a single client until the buffer size is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. @@ -175,7 +181,7 @@ var settings = new PersistentSubscriptionSettings( checkPointLowerBound: 20 ); -await client.UpdateToStreamAsync("example-stream", "subscription-group", settings); +await client.UpdateToStreamAsync("order-123", "subscription-group", settings); ``` | Parameter | Description | @@ -212,7 +218,7 @@ Remove a subscription group with the delete operation. Like the creation of grou ```cs try { - await client.DeleteToStreamAsync("example-stream", "subscription-group"); + await client.DeleteToStreamAsync("order-123", "subscription-group"); } catch (PersistentSubscriptionNotFoundException) { Console.WriteLine("Subscription group does not exist."); } catch (Exception ex) { diff --git a/docs/api/projections.md b/docs/api/projections.md index e7df15363..ac4c10d7b 100644 --- a/docs/api/projections.md +++ b/docs/api/projections.md @@ -1,10 +1,6 @@ --- order: 6 title: Projections -head: - - - title - - {} - - Projections | .NET | Clients | Kurrent Docs --- # Projection management @@ -18,10 +14,9 @@ For a detailed explanation of projections, see the [server documentation](@serve Projection management operations are exposed through the dedicated client. ```cs -var settings = KurrentDBClientSettings.Create(connection); -settings.ConnectionName = "Projection management client"; -settings.DefaultCredentials = new UserCredentials("admin", "changeit"); -var managementClient = new KurrentDBProjectionManagementClient(settings); +var client = new KurrentDBProjectionManagementClient( + KurrentDBClientSettings.Create("kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false") +); ``` ## Create a projection @@ -45,7 +40,7 @@ const string js = """ .outputState(); """; -await managementClient.CreateContinuousAsync("count-events", js); +await client.CreateContinuousAsync("count-events", js); ``` Trying to create projections with the same name will result in an error: @@ -53,9 +48,9 @@ Trying to create projections with the same name will result in an error: ```cs var name = "count-events"; -await managementClient.CreateContinuousAsync(name, js); +await client.CreateContinuousAsync(name, js); try { - await managementClient.CreateContinuousAsync(name, js); + await client.CreateContinuousAsync(name, js); } catch (RpcException e) when (e.StatusCode is StatusCode.AlreadyExists) { Console.WriteLine(e.Message); @@ -71,7 +66,7 @@ catch (RpcException e) when (e.Message.Contains("Conflict")) { // will be remove It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```cs -await managementClient.RestartSubsystemAsync(); +await client.RestartSubsystemAsync(); ``` ## Enable a projection @@ -81,14 +76,14 @@ Once enabled, the projection will start to process events even after restarting You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```cs -await managementClient.EnableAsync("$by_category"); +await client.EnableAsync("$by_category"); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```cs try { - await managementClient.EnableAsync("projection that does not exists"); + await client.EnableAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); @@ -105,14 +100,14 @@ Once disabled, the projection will not process events even after restarting the You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```cs -await managementClient.DisableAsync("$by_category"); +await client.DisableAsync("$by_category"); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```cs try { - await managementClient.DisableAsync("projection that does not exists"); + await client.DisableAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); @@ -131,14 +126,14 @@ This feature is currently not supported by the client. Aborts a projection, this will not save the projection's checkpoint. ```cs -await managementClient.AbortAsync("countEvents_Abort"); +await client.AbortAsync("countEvents_Abort"); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```cs try { - await managementClient.AbortAsync("projection that does not exists"); + await client.AbortAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); @@ -153,14 +148,14 @@ catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be remove Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```cs -await managementClient.ResetAsync("countEvents_Reset"); +await client.ResetAsync("countEvents_Reset"); ``` Resetting a projection that does not exist will result in an error. ```cs try { - await managementClient.ResetAsync("projection that does not exists"); + await client.ResetAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); @@ -192,15 +187,15 @@ fromAll() var name = "count-events"; -await managementClient.CreateContinuousAsync(name, "fromAll().when()"); -await managementClient.UpdateAsync(name, js); +await client.CreateContinuousAsync(name, "fromAll().when()"); +await client.UpdateAsync(name, js); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```cs try { - await managementClient.UpdateAsync("Update Not existing projection", "fromAll().when()"); + await client.UpdateAsync("Update Not existing projection", "fromAll().when()"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); @@ -216,7 +211,7 @@ Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs -var details = managementClient.ListAllAsync(); +var details = client.ListAllAsync(); await foreach (var detail in details) Console.WriteLine( @@ -230,7 +225,7 @@ Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs -var details = managementClient.ListContinuousAsync(); +var details = client.ListContinuousAsync(); await foreach (var detail in details) Console.WriteLine( $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" @@ -242,7 +237,7 @@ await foreach (var detail in details) Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. -```cs +```cs{18} const string js = """ fromAll() .when({ @@ -258,8 +253,10 @@ fromAll() var name = "count-events"; -await managementClient.CreateContinuousAsync(name, js); -var status = await managementClient.GetStatusAsync(name); +await client.CreateContinuousAsync(name, js); + +var status = await client.GetStatusAsync(name); + Console.WriteLine( $@"{status?.Name}, {status?.Status}, {status?.CheckpointStatus}, {status?.Mode}, {status?.Progress}" ); @@ -269,7 +266,7 @@ Console.WriteLine( Retrieves the state of a projection. -```cs +```cs{20} const string js = """ fromAll() .when({ @@ -285,31 +282,32 @@ fromAll() var name = $"count-events"; -await managementClient.CreateContinuousAsync(name, js); +await client.CreateContinuousAsync(name, js); -//give it some time to process and have a state. -await Task.Delay(500); +await Task.Delay(500); // give it some time to process and have a state. -var stateDocument = await managementClient.GetStateAsync(name); -var result = await managementClient.GetStateAsync(name); +var document = await client.GetStateAsync(name); -Console.WriteLine(DocToString(stateDocument)); -Console.WriteLine(result); +Console.WriteLine(document.RootElement.GetRawText()) +``` -static async Task DocToString(JsonDocument d) { - await using var stream = new MemoryStream(); - var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false }); - d.WriteTo(writer); - await writer.FlushAsync(); - return Encoding.UTF8.GetString(stream.ToArray()); -} +or you can retrieve the state as a typed result: + +```cs +public class Result { + public int count { get; set; } + + public override string ToString() => $"count= {count}"; +}; + +var result = await client.GetStateAsync(name); ``` ## Get result Retrieves the result of the named projection and partition. -```cs +```cs{20} const string js = """ fromAll() .when({ @@ -325,24 +323,25 @@ fromAll() var name = "count-events"; -await managementClient.CreateContinuousAsync(name, js); +await client.CreateContinuousAsync(name, js); await Task.Delay(500); //give it some time to have a result. -// Results are retrieved either as JsonDocument or a typed result -var document = await managementClient.GetResultAsync(name); -var result = await managementClient.GetResultAsync(name); +var document = await client.GetResultAsync(name); -Console.WriteLine(DocToString(document)); -Console.WriteLine(result); +Console.WriteLine(document.RootElement.GetRawText()) +``` -static string DocToString(JsonDocument d) { - using var stream = new MemoryStream(); - using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false }); - d.WriteTo(writer); - writer.Flush(); - return Encoding.UTF8.GetString(stream.ToArray()); -} +or it can be retrieved as a typed result: + +```cs +public class Result { + public int count { get; set; } + + public override string ToString() => $"count= {count}"; +}; + +var result = await client.GetResultAsync(name); ``` ## Projection Details diff --git a/docs/api/reading-events.md b/docs/api/reading-events.md index f1093975a..c08f51add 100644 --- a/docs/api/reading-events.md +++ b/docs/api/reading-events.md @@ -1,9 +1,5 @@ --- order: 3 -head: - - - title - - {} - - Reading Events | .NET | Clients | Kurrent Docs --- # Reading Events @@ -16,10 +12,6 @@ Each event in KurrentDB belongs to an individual stream. When reading events, pi All events have a `StreamPosition` and a `Position`. `StreamPosition` is a *big int* (unsigned 64-bit integer) and represents the place of the event in the stream. `Position` is the event's logical position, and is represented by `CommitPosition` and a `PreparePosition`. Note that when reading events you will supply a different "position" depending on whether you are reading from an individual stream or the `$all` stream. -:::tip -Check [connecting to KurrentDB instructions](getting-started.md#required-packages) to learn how to configure and use the client SDK. -::: - ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. @@ -30,14 +22,14 @@ The simplest way to read a stream forwards is to supply a stream name, read dire ```cs -var events = client.ReadStreamAsync(Direction.Forwards, "example-stream", StreamPosition.Start); +var events = client.ReadStreamAsync(Direction.Forwards, "order-123", StreamPosition.Start); ``` This will return an enumerable that can be iterated on: ```cs -await foreach (var @event in events) - Console.WriteLine(Encoding.UTF8.GetString(@event.Event.Data.ToArray())); +await foreach (var e in events) + Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` There are a number of additional arguments you can provide when reading a stream, listed below. @@ -58,34 +50,48 @@ You can use the `configureOperationOptions` argument to provide a function that The `userCredentials` argument is optional. It is used to override the default credentials specified when creating the client instance. -```cs +```cs{5} var result = client.ReadStreamAsync( Direction.Forwards, - "example-stream", + "order-123", StreamPosition.Start, - userCredentials: new UserCredentials("admin", "changeit"), - cancellationToken: cancellationToken + userCredentials: new UserCredentials("admin", "changeit") ); ``` ### Reading from a revision -Instead of providing the `StreamPosition` you can also provide a specific stream revision as a *big int* (unsigned 64-bit integer). +Instead of providing the `StreamPosition` you can also provide a specific stream revision as a big int (unsigned 64-bit integer). You can use `FirstStreamPosition` and `LastStreamPosition` from a previous read result as the starting revision. -```cs -Console.WriteLine(events.FirstStreamPosition); -Console.WriteLine(events.LastStreamPosition); +```cs{11} +var orders = client.ReadStreamAsync( + Direction.Forwards, + "order-123", + StreamPosition.Start +); + +if (orders.FirstStreamPosition is not null) { + var customers = client.ReadStreamAsync( + Direction.Forwards, + "customer-456", + orders.FirstStreamPosition + ); +} ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the *stream position* to the end: -```cs -var events = client.ReadStreamAsync(Direction.Backwards, "example-stream", StreamPosition.End); +```cs{2} +var events = client.ReadStreamAsync( + Direction.Backwards, + "order-123", + StreamPosition.End +); await foreach (var e in events) - Console.WriteLine(Encoding.UTF8.GetString(e.Event.Data.ToArray())); + Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` :::tip @@ -100,13 +106,15 @@ It is important to check the value of this field before attempting to iterate an For example: -```cs -var result = client.ReadStreamAsync(Direction.Forwards, "example-stream", 10, 20); +```cs{5} +var result = client.ReadStreamAsync( + Direction.Forwards, "order-123", revision: 10, maxCount: 20 +); if (await result.ReadState == ReadState.StreamNotFound) return; await foreach (var e in result) - Console.WriteLine(Encoding.UTF8.GetString(e.Event.Data.ToArray())); + Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` ## Reading from the $all stream @@ -125,7 +133,7 @@ You can iterate asynchronously through the result: ```cs await foreach (var e in events) - Console.WriteLine(Encoding.UTF8.GetString(e.Event.Data.ToArray())); + Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` There are a number of additional arguments you can provide when reading the `$all` stream. @@ -138,12 +146,11 @@ Passing in the max count allows you to limit the number of events that returned. When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. -```cs +```cs{4} var result = client.ReadAllAsync( Direction.Forwards, Position.Start, - resolveLinkTos: true, - cancellationToken: cancellationToken + resolveLinkTos: true ); ``` @@ -154,7 +161,7 @@ This argument is generic setting class for all operations that can be set on all #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. -```cs +```cs{4} var result = client.ReadAllAsync( Direction.Forwards, Position.Start, @@ -181,13 +188,13 @@ KurrentDB will also return system events when reading from the `$all` stream. In All system events begin with `$` or `$$` and can be easily ignored by checking the `EventType` property. -```cs +```cs{4} var events = client.ReadAllAsync(Direction.Forwards, Position.Start); await foreach (var e in events) { if (e.Event.EventType.StartsWith("$")) continue; - Console.WriteLine(Encoding.UTF8.GetString(e.Event.Data.ToArray())); + Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); } ``` diff --git a/docs/api/subscriptions.md b/docs/api/subscriptions.md index a6a9fc0f1..dd83cf7d7 100644 --- a/docs/api/subscriptions.md +++ b/docs/api/subscriptions.md @@ -1,77 +1,50 @@ --- order: 4 -head: - - - title - - {} - - Catch-up Subscriptions | .NET | Clients | Kurrent Docs --- # Catch-up Subscriptions -Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. - -You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. +Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. -:::tip -Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. -::: - -## Subscribing from the start - -If you need to process all the events in the store, including historical events, you'll need to subscribe from the beginning. You can either subscribe to receive events from a single stream or subscribe to `$all` if you need to process all events in the database. +## Basic Subscriptions -### Subscribing to a stream +You can subscribe to a single stream or to `$all` to process all events in the database. -The simplest stream subscription looks like the following : +**Stream subscription:** ```cs -await using var subscription = client.SubscribeToStream( - "example-stream", - FromStream.Start, - cancellationToken: ct -); +await using var subscription = client.SubscribeToStream("order-123", FromStream.Start); await foreach (var message in subscription.Messages.WithCancellation(ct)) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); - await HandleEvent(evnt); break; } } ``` -The provided handler will be called for every event in the stream. - -When you subscribe to a stream with link events, for example the `$ce` category stream, you need to set `resolveLinkTos` to `true`. Read more about it [below](#resolving-link-to-s). - -### Subscribing to `$all` - -Subscribing to `$all` is similar to subscribing to a single stream. The handler will be called for every event appended after the starting position. +**`$all` subscription:** ```cs -await using var subscription = client.SubscribeToAll( - FromAll.Start, - cancellationToken: ct -); +await using var subscription = client.SubscribeToAll(FromAll.Start); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); - await HandleEvent(evnt); break; } } ``` -## Subscribing from a specific position +When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. -The previous examples subscribed to the stream from the beginning. That subscription invoked the handler for every event in the stream before waiting for new events. +## Subscribing from a Position -Both stream and $all subscriptions accept a starting position if you want to read from a specific point onward. If events already exist at the position you subscribe to, they will be read on the server side and sent to the subscription. +Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist at the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. @@ -79,40 +52,32 @@ Once caught up, the server will push any new events received on the streams to t The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: -### Subscribing to a stream - -To subscribe to a stream from a specific position, you must provide a *stream position*. This can be `Start`, `End` or a *big int* (unsigned 64 bit integer) position. +**Stream from specific position:** -The following subscribes to the stream `example-stream` at position `20`, this means that events `21` and onward will be handled: +To subscribe to a stream from a specific position, provide a stream position (`Start`, `End` or a 64-bit unsigned integer representing the stream revision): -```cs +```cs{3} await using var subscription = client.SubscribeToStream( - "example-stream", - FromStream.After(StreamPosition.FromInt64(20)), - cancellationToken: ct + "order-123", + FromStream.After(StreamPosition.FromInt64(20)) ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); - await HandleEvent(evnt); break; } } ``` -### Subscribing to $all - -Subscribing to the `$all` stream is similar to subscribing to a regular stream. The difference is how to specify the starting position. For the `$all` stream, provide a `Position` structure that consists of two big integers: the prepare and commit positions. Use `Start`, `End`, or create a `Position` from specific commit and prepare values. +**`$all` from specific position:** -The corresponding `$all` subscription will subscribe from the event after the one at commit position `1056` and prepare position `1056`. +For the `$all` stream, provide a `Position` structure with prepare and commit positions: -Please note that this position will need to be a legitimate position in `$all`. - -```cs +```cs{10} var result = await client.AppendToStreamAsync( - "example-stream", + "order-123", StreamState.NoStream, [ new EventData(Uuid.NewUuid(), "-", ReadOnlyMemory.Empty) @@ -120,60 +85,30 @@ var result = await client.AppendToStreamAsync( ); await using var subscription = client.SubscribeToAll( - FromAll.After(result.LogPosition), - cancellationToken: ct + FromAll.After(result.LogPosition) ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); - await HandleEvent(evnt); break; } } ``` -## Subscribing to a stream for live updates +**Live updates only:** -You can subscribe to a stream to get live updates by subscribing to the end of the stream: +Subscribe to the end of a stream to get only new events: ```cs -await using var subscription = client.SubscribeToStream( - "example-stream", - FromStream.End, - cancellationToken: ct -); +// Stream +await using var subscription = client.SubscribeToStream("order-123", FromStream.End); -await foreach (var message in subscription.Messages) { - switch (message) { - case StreamMessage.Event(var evnt): - Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); - await HandleEvent(evnt); - break; - } -} +// $all +await using var subscription = client.SubscribeToAll(FromAll.End); ``` -And the same works with `$all` : - -```cs -var subscription = client.SubscribeToAll(FromAll.End, cancellationToken: ct); - -await foreach (var message in subscription.Messages) { - switch (message) { - case StreamMessage.Event(var evnt): - Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); - await HandleEvent(evnt); - break; - } -} -``` - -This will not read through the history of the stream but will notify the handler when a new event appears in the respective stream. - -Keep in mind that when you subscribe to a stream from a specific position, as described [above](#subscribing-from-a-specific-position), you will also get live updates after your subscription catches up (processes all the historical events). - ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. @@ -184,25 +119,23 @@ Link-to events point to events in other streams in KurrentDB. These are generall When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: -```cs +```cs{4} await using var subscription = client.SubscribeToStream( - "$et-myEventType", + "$et-order", FromStream.Start, - resolveLinkTos: true, - cancellationToken: ct + resolveLinkTos: true ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); - await HandleEvent(evnt); break; } } ``` -## Dropped subscriptions +## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides a `subscriptionDropped` callback, which will get called when the subscription breaks. @@ -218,208 +151,147 @@ The possible reasons for a subscription to drop are: Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. -### Handling subscription drops +### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: -```cs -var checkpoint = await ReadStreamCheckpointAsync() switch { - null => FromStream.Start, - var position => FromStream.After(position.Value) -}; - -try { - await using var subscription = client.SubscribeToStream( - "example-stream", - checkpoint, - cancellationToken: ct - ); - - await foreach (var message in subscription.Messages) { - switch (message) { - case StreamMessage.Event(var evnt): - Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); - await HandleEvent(evnt); - checkpoint = FromStream.After(evnt.OriginalEventNumber); - break; - } - } -} catch (OperationCanceledException) { - Console.WriteLine($"Subscription was canceled."); -} catch (ObjectDisposedException) { - Console.WriteLine($"Subscription was canceled by the user."); -} catch (Exception ex) { - Console.WriteLine($"Subscription was dropped: {ex}"); - goto Subscribe; -} -``` +```cs{1,10} +var checkpoint = FromStream.Start; // or read from a persistent store -When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: +await using var subscription = client.SubscribeToStream("order-123", checkpoint); -```cs -var checkpoint = await ReadCheckpointAsync() switch { - null => FromAll.Start, - var position => FromAll.After(position.Value) -}; - -try { - await using var subscription = client.SubscribeToAll( - checkpoint, - cancellationToken: ct); - await foreach (var message in subscription.Messages) { - switch (message) { - case StreamMessage.Event(var evnt): - Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); - await HandleEvent(evnt); - if (evnt.OriginalPosition is not null) { - checkpoint = FromAll.After(evnt.OriginalPosition.Value); - } - - break; - } +await foreach (var message in subscription.Messages) { + switch (message) { + case StreamMessage.Event(var evnt): + Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); + + checkpoint = FromStream.After(evnt.OriginalEventNumber); + + break; } -} catch (OperationCanceledException) { - Console.WriteLine($"Subscription was canceled."); -} catch (ObjectDisposedException) { - Console.WriteLine($"Subscription was canceled by the user."); -} catch (Exception ex) { - Console.WriteLine($"Subscription was dropped: {ex}"); - goto Subscribe; } ``` -## User credentials +When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: -The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. +```cs{1,13} +var checkpoint = FromAll.Start; // or read from a persistent store -The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. - -```cs -await using var subscription = client.SubscribeToAll( - FromAll.Start, - userCredentials: new UserCredentials("admin", "changeit"), - cancellationToken: ct -); +await using var subscription = client.SubscribeToAll(checkpoint); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); - await HandleEvent(evnt); + + if (evnt.OriginalPosition is not null) + checkpoint = FromAll.After(evnt.OriginalPosition.Value); break; } } ``` -## Server-side filtering - -KurrentDB allows you to filter the events whilst subscribing to the `$all` stream to only receive the events you care about. - -You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. +## Handling Subscription State Changes -::: tip -Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. +::: info KurrentDB 23.10.0+ +This feature requires KurrentDB version 23.10.0 or later. ::: -A simple stream prefix filter looks like this: - -```cs -var filter = new SubscriptionFilterOptions(StreamFilter.Prefix("test-", "other-")); +When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `CaughtUp` message on the subscription. -await using var subscription = client.SubscribeToAll( - FromAll.Start, - filterOptions: filter, - cancellationToken: ct -); +```cs{8-10} +await using var subscription = client.SubscribeToStream("order-123", FromStream.Start); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); - await HandleEvent(evnt); - break; - case StreamMessage.AllStreamCheckpointReached(var position): - Console.WriteLine($"Checkpoint reached: {position}"); + case StreamMessage.CaughtUp: + Console.WriteLine("Caught up to live mode"); break; } } ``` -The filtering API is described more in-depth in the [filtering section](subscriptions.md#server-side-filtering). +::: tip +The `CaughtUp` message is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this message will be emitted right away. +::: -### Filtering out system events +## User credentials -There are events in KurrentDB called system events. These are prefixed with a `$` and under most circumstances you won't care about these. They can be filtered out by passing in a `SubscriptionFilterOptions` when subscribing to the `$all` stream. +The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. -```cs +The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. + +```cs{3} await using var subscription = client.SubscribeToAll( FromAll.Start, - filterOptions: new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()) + userCredentials: new UserCredentials("admin", "changeit") ); - -await foreach (var message in subscription.Messages) { - switch (message) { - case StreamMessage.Event(var e): - Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.CommitPosition}"); - break; - } -} ``` +## Server-side Filtering + +KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. + ::: tip -`$stats` events are no longer stored in KurrentDB by default so there won't be as many `$` events as before. +Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: -### Filtering by event type +**Basic filtering:** -If you only want to subscribe to events of a given type, there are two options. You can either use a regular expression or a prefix. +```cs +await using var subscription = client.SubscribeToAll( + FromAll.Start, + filterOptions: new SubscriptionFilterOptions(StreamFilter.Prefix("test-", "other-")) +); +``` -#### Filtering by prefix +### Filtering out system events -If you want to filter by prefix, pass in a `SubscriptionFilterOptions` to the subscription with an `EventTypeFilter.Prefix`. +System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```cs -var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-")); +await using var subscription = client.SubscribeToAll( + FromAll.Start, + filterOptions: new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()) +); ``` -This will only subscribe to events with a type that begin with `customer-`. - -#### Filtering by regular expression +### Filtering by event type -It might be advantageous to provide a regular expression when you want to subscribe to multiple event types. +**By prefix:** ```cs -var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.RegularExpression("^user|^company")); +var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-")); ``` -This will subscribe to any event that begins with `user` or `company`. +**By regular expression:** -### Filtering by stream name - -To subscribe to a stream by name, choose either a regular expression or a prefix. +```cs +var filterOptions = new SubscriptionFilterOptions( + EventTypeFilter.RegularExpression("^user|^company") +); +``` -#### Filtering by prefix +### Filtering by stream name -If you want to filter by prefix, pass in a `SubscriptionFilterOptions` to the subscription with an `StreamFilter.Prefix`. +**By prefix:** ```cs var filterOptions = new SubscriptionFilterOptions(StreamFilter.Prefix("user-")); ``` -This will only subscribe to all streams with a name that begins with `user-`. - -#### Filtering by regular expression - -To subscribe to multiple streams, use a regular expression. +**By regular expression:** ```cs -var filterOptions = new SubscriptionFilterOptions(StreamFilter.RegularExpression("^account|^savings")); +var filterOptions = new SubscriptionFilterOptions( + StreamFilter.RegularExpression("^account|^savings") +); ``` -This will subscribe to any stream with a name that begins with `account` or `savings`. - ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. @@ -437,9 +309,10 @@ If your database contains events created by the legacy TCP client using the [tra ::: ### Updating checkpoints at regular intervals + The client SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. -```cs +```cs{10-13} var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()); await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); @@ -460,10 +333,14 @@ await foreach (var message in subscription.Messages) { By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval -You can adjust the checkpoint interval to change how often the client is notified. -```cs -var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents(), 1000); +You can adjust the checkpoint interval to change how often the client is notified. + +```cs{3} +var filterOptions = new SubscriptionFilterOptions( + filter: EventTypeFilter.ExcludeSystemEvents(), + checkpointInterval: 1000 +); ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. From f6d402d0ddebc6e5a44011f9f7cef8295b1d0e75 Mon Sep 17 00:00:00 2001 From: William Chong Date: Thu, 24 Jul 2025 13:25:17 +0400 Subject: [PATCH 03/29] chore: Ignore node modules --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b21b5418b..1f0b1b9da 100644 --- a/.gitignore +++ b/.gitignore @@ -366,4 +366,6 @@ certs-cluster/ .DS_Store docs/public -.temp \ No newline at end of file +.temp +.cache +node_modules From af888bee471f4113b010104ab37dbbaf12c88e77 Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 6 Aug 2025 15:03:17 +0400 Subject: [PATCH 04/29] Trigger CI From 77e674b8b215033f2864c30010ee1391cf6d043b Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 6 Aug 2025 15:10:43 +0400 Subject: [PATCH 05/29] Revamp github workflow --- .github/workflows/base.yml | 88 --- .github/workflows/build-docs.yml | 14 + .../workflows/cherry-pick-pr-for-label.yml | 14 + .github/workflows/ci.yml | 37 -- .github/workflows/dispatch-ce.yml | 29 - .github/workflows/dispatch-ee.yml | 31 -- .github/workflows/dotnet.yml | 19 + .github/workflows/load-configuration.yml | 66 +++ .github/workflows/publish.yml | 502 +++++++++--------- .github/workflows/test.yml | 83 +++ 10 files changed, 447 insertions(+), 436 deletions(-) delete mode 100644 .github/workflows/base.yml create mode 100644 .github/workflows/build-docs.yml create mode 100644 .github/workflows/cherry-pick-pr-for-label.yml delete mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/dispatch-ce.yml delete mode 100644 .github/workflows/dispatch-ee.yml create mode 100644 .github/workflows/dotnet.yml create mode 100644 .github/workflows/load-configuration.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml deleted file mode 100644 index 7038adeba..000000000 --- a/.github/workflows/base.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Build - -on: - workflow_call: - inputs: - kurrentdb-tag: - description: The docker tag to use. If kurrentdb-image is empty, the action will use the values in the KURRENTDB_DOCKER_IMAGES variable (ci, lts, previous-lts). - required: true - type: string - kurrentdb-image: - description: The docker image to use. Leave this empty to use the image in the KURRENTDB_DOCKER_IMAGES variable. - required: false - type: string - kurrentdb-registry: - description: The docker registry to use. Leave this empty to use the registry in the KURRENTDB_DOCKER_IMAGES variable. - required: false - type: string - test: - description: Which test to run. - required: true - type: string -env: - KURRENTDB_TAG: ${{ inputs.kurrentdb-image != '' && inputs.kurrentdb-tag || fromJSON(vars.KURRENTDB_DOCKER_IMAGES)[inputs.kurrentdb-tag].tag }} - KURRENTDB_IMAGE: ${{ inputs.kurrentdb-image || fromJSON(vars.KURRENTDB_DOCKER_IMAGES)[inputs.kurrentdb-tag].image }} - KURRENTDB_REGISTRY: ${{ inputs.kurrentdb-registry || fromJSON(vars.KURRENTDB_DOCKER_IMAGES)[inputs.kurrentdb-tag].registry }} -jobs: - test: - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - framework: [ net8.0, net9.0 ] - os: [ ubuntu-latest ] - configuration: [ release ] - runs-on: ${{ matrix.os }} - name: ${{ inputs.test }} (${{ matrix.os }}, ${{ matrix.framework }}) - steps: - - name: Echo docker details - shell: bash - run: | - echo "${{env.KURRENTDB_REGISTRY}}" - echo "${{env.KURRENTDB_IMAGE}}" - echo "${{env.KURRENTDB_TAG}}" - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Login to Cloudsmith - uses: docker/login-action@v3 - with: - registry: docker.kurrent.io - username: ${{ secrets.CLOUDSMITH_CICD_USER }} - password: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} - - - name: Pull KurrentDB Image - shell: bash - run: | - docker pull ${{ env.KURRENTDB_REGISTRY }}/${{ env.KURRENTDB_IMAGE}}:${{ env.KURRENTDB_TAG}} - - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - 9.0.x - - - name: Generate certificates - shell: bash - run: sudo ./gencert.sh - - - name: Restore dependencies - shell: bash - run: dotnet restore - - - name: Run Tests - shell: bash - env: - ES_DOCKER_TAG: ${{env.KURRENTDB_TAG}} - ES_DOCKER_REGISTRY: ${{env.KURRENTDB_REGISTRY}}/${{env.KURRENTDB_IMAGE}} - KURRENTDB_LICENSE_KEY: ${{ secrets.KURRENTDB_TEST_LICENSE_KEY }} - run: | - dotnet test --configuration ${{ matrix.configuration }} --blame \ - --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ - --framework ${{ matrix.framework }} \ - --filter "Category=Target:${{ inputs.test }}" \ - test/KurrentDB.Client.Tests \ No newline at end of file diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml new file mode 100644 index 000000000..75f0a3c90 --- /dev/null +++ b/.github/workflows/build-docs.yml @@ -0,0 +1,14 @@ +name: Build Production Site + +on: + push: + branches: [release/**/**] + paths: + - '**.md' + +jobs: + dispatch: + runs-on: ubuntu-latest + steps: + - name: Trigger build + run: curl -X POST -d {} "${{ secrets.CLOUDFLARE_BUILD_HOOK }}" diff --git a/.github/workflows/cherry-pick-pr-for-label.yml b/.github/workflows/cherry-pick-pr-for-label.yml new file mode 100644 index 000000000..b60bec492 --- /dev/null +++ b/.github/workflows/cherry-pick-pr-for-label.yml @@ -0,0 +1,14 @@ +name: Cherry pick PR commits for label +on: + pull_request_target: + types: [closed] +jobs: + cherry_pick_pr_for_label: + name: Cherry pick PR commits for label + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Cherry Pick PR for label + uses: kurrent-io/Automations/cherry-pick-pr-for-label@master + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 80e1c7ead..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: CI - -on: - pull_request: - push: - branches: - - master - tags: - - v* - workflow_dispatch: - -jobs: - ce: - uses: ./.github/workflows/base.yml - strategy: - fail-fast: false - matrix: - kurrentdb-tag: [ ci, lts ] - test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement, Plugins, Security, Misc ] - name: Test (${{ matrix.kurrentdb-tag }}) - with: - kurrentdb-tag: ${{ matrix.kurrentdb-tag }} - test: ${{ matrix.test }} - secrets: inherit - - ee: - uses: ./.github/workflows/base.yml - strategy: - fail-fast: false - matrix: - kurrentdb-tag: [ previous-lts ] - test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement, Security, Misc ] - name: Test (${{ matrix.kurrentdb-tag }}) - with: - kurrentdb-tag: ${{ matrix.kurrentdb-tag }} - test: ${{ matrix.test }} - secrets: inherit diff --git a/.github/workflows/dispatch-ce.yml b/.github/workflows/dispatch-ce.yml deleted file mode 100644 index dc76992af..000000000 --- a/.github/workflows/dispatch-ce.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Dispatch CE - -on: - workflow_dispatch: - inputs: - docker-tag: - description: "Docker tag" - required: true - type: string - docker-image: - description: "Docker image" - required: true - type: string - -jobs: - test: - uses: ./.github/workflows/base.yml - strategy: - fail-fast: false - matrix: - test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement, Security, Misc ] - name: Test CE (${{ inputs.docker-tag }}) - with: - docker-tag: ${{ inputs.docker-tag }} - docker-image: ${{ inputs.docker-image }} - test: ${{ matrix.test }} - secrets: - CLOUDSMITH_CICD_USER: ${{ secrets.CLOUDSMITH_CICD_USER }} - CLOUDSMITH_CICD_TOKEN: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} diff --git a/.github/workflows/dispatch-ee.yml b/.github/workflows/dispatch-ee.yml deleted file mode 100644 index 810dc24c4..000000000 --- a/.github/workflows/dispatch-ee.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Dispatch - -on: - workflow_dispatch: - inputs: - kurrentdb-tag: - description: "The KurrentDB docker tag to use. If kurrentdb-image is empty, the action will use the values in the KURRENTDB_DOCKER_IMAGES variable (ci, lts, previous-lts)." - required: true - type: string - kurrentdb-image: - description: "The KurrentDB docker image to test against. Leave this empty to use the image in the KURRENTDB_DOCKER_IMAGES variable" - required: false - type: string - kurrentdb-registry: - description: "The docker registry containing the KurrentDB docker image. Leave this empty to use the registry in the KURRENTDB_DOCKER_IMAGES variable." - required: false - type: string -jobs: - test: - uses: ./.github/workflows/base.yml - strategy: - fail-fast: false - matrix: - test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement, Plugins ] - name: Test (${{ inputs.kurrentdb-tag }}) - with: - kurrentdb-tag: ${{ inputs.kurrentdb-tag }} - kurrentdb-image: ${{ inputs.kurrentdb-image }} - kurrentdb-registry: ${{ inputs.kurrentdb-registry }} - test: ${{ matrix.test }} - secrets: inherit diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 000000000..356cfce04 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,19 @@ +name: Client + +on: + pull_request: + push: + branches: + - master + +jobs: + old: + name: ${{ matrix.runtime }} + uses: ./.github/workflows/test.yml + strategy: + fail-fast: false + matrix: + runtime: [ previous-lts, lts, ci ] + with: + runtime: ${{ matrix.runtime }} + secrets: inherit diff --git a/.github/workflows/load-configuration.yml b/.github/workflows/load-configuration.yml new file mode 100644 index 000000000..67ef6464c --- /dev/null +++ b/.github/workflows/load-configuration.yml @@ -0,0 +1,66 @@ +name: Load KurrentDB Runtime Configuration +on: + workflow_call: + inputs: + runtime: + description: "The runtime's name. Current options are: `ci`, `previous-lts`, `latest` and `qa`" + required: true + type: string + + registry: + description: "When in QA mode, the Docker registry to use" + type: string + required: false + + image: + description: "When in QA mode, the Docker image to use" + type: string + required: false + + tag: + description: "When in QA mode, the Docker image tag to use" + type: string + required: false + + outputs: + runtime: + description: The runtime's name + value: ${{ inputs.runtime }} + + registry: + description: The Docker registry + value: ${{ jobs.load.outputs.registry }} + + image: + description: The Docker image + value: ${{ jobs.load.outputs.image }} + + tag: + description: The Docker image tag + value: ${{ jobs.load.outputs.tag }} + +jobs: + load: + runs-on: ubuntu-latest + outputs: + registry: ${{ steps.set.outputs.registry }} + image: ${{ steps.set.outputs.image }} + tag: ${{ steps.set.outputs.tag }} + + steps: + - name: Set KurrentDB Runtime Configuration Properties + id: set + run: | + case ${{ inputs.runtime }} in + "qa") + echo "registry=${{ inputs.registry }}" >> $GITHUB_OUTPUT + echo "image=${{ inputs.image }}" >> $GITHUB_OUTPUT + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + ;; + + *) + echo "registry=${{ fromJSON(vars.KURRENTDB_DOCKER_IMAGES)[inputs.runtime].registry }}" >> $GITHUB_OUTPUT + echo "image=${{ fromJSON(vars.KURRENTDB_DOCKER_IMAGES)[inputs.runtime].image }}" >> $GITHUB_OUTPUT + echo "tag=${{ fromJSON(vars.KURRENTDB_DOCKER_IMAGES)[inputs.runtime].tag }}" >> $GITHUB_OUTPUT + ;; + esac diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ee39542d3..9616ef978 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,267 +1,267 @@ -name: Publish +# name: Publish -on: - pull_request: - push: - branches: - - master - tags: - - v* - workflow_dispatch: - inputs: - ref: - description: "Git reference (branch, tag, or commit SHA)" - required: true - default: "master" - dry_run: - description: "Dry run" - required: true - type: boolean - default: true - version: - description: "Version number" - required: false - default: "" +# on: +# pull_request: +# push: +# branches: +# - master +# tags: +# - v* +# workflow_dispatch: +# inputs: +# ref: +# description: "Git reference (branch, tag, or commit SHA)" +# required: true +# default: "master" +# dry_run: +# description: "Dry run" +# required: true +# type: boolean +# default: true +# version: +# description: "Version number" +# required: false +# default: "" -jobs: - vulnerability-scan: - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - framework: [net8.0, net9.0] - os: [ubuntu-latest, windows-latest] - runs-on: ${{ matrix.os }} - name: scan-vulnerabilities/${{ matrix.os }}/${{ matrix.framework }} - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: ${{ github.event.inputs.ref || github.ref }} - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - 9.0.x - - name: Scan for Vulnerabilities - shell: bash - run: | - dotnet nuget list source - dotnet restore ./src/KurrentDB.Client/KurrentDB.Client.csproj - dotnet restore ./test/KurrentDB.Client.Tests/KurrentDB.Client.Tests.csproj - dotnet list package --vulnerable --include-transitive --framework ${{ matrix.framework }} | tee vulnerabilities.txt - ! cat vulnerabilities.txt | grep -q "has the following vulnerable packages" +# jobs: +# vulnerability-scan: +# timeout-minutes: 10 +# strategy: +# fail-fast: false +# matrix: +# framework: [net8.0, net9.0] +# os: [ubuntu-latest, windows-latest] +# runs-on: ${{ matrix.os }} +# name: scan-vulnerabilities/${{ matrix.os }}/${{ matrix.framework }} +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# with: +# ref: ${{ github.event.inputs.ref || github.ref }} +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# 9.0.x +# - name: Scan for Vulnerabilities +# shell: bash +# run: | +# dotnet nuget list source +# dotnet restore ./src/KurrentDB.Client/KurrentDB.Client.csproj +# dotnet restore ./test/KurrentDB.Client.Tests/KurrentDB.Client.Tests.csproj +# dotnet list package --vulnerable --include-transitive --framework ${{ matrix.framework }} | tee vulnerabilities.txt +# ! cat vulnerabilities.txt | grep -q "has the following vulnerable packages" - build-samples: - timeout-minutes: 5 - name: build-samples/${{ matrix.framework }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - framework: [net8.0, net9.0] - services: - esdb: - image: docker.kurrent.io/eventstore/eventstoredb-ee:lts - env: - EVENTSTORE_INSECURE: true - EVENTSTORE_MEM_DB: false - EVENTSTORE_RUN_PROJECTIONS: all - EVENTSTORE_START_STANDARD_PROJECTIONS: true - ports: - - 2113:2113 - options: --health-cmd "exit 0" - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: ${{ github.event.inputs.ref || github.ref }} - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - 9.0.x - - name: Compile - shell: bash - run: | - dotnet build samples - - name: Run - shell: bash - run: | - find samples/ -type f -iname "*.csproj" -print0 | xargs -0L1 dotnet run --framework ${{ matrix.framework }} --project +# build-samples: +# timeout-minutes: 5 +# name: build-samples/${{ matrix.framework }} +# runs-on: ubuntu-latest +# strategy: +# fail-fast: false +# matrix: +# framework: [net8.0, net9.0] +# services: +# esdb: +# image: docker.kurrent.io/eventstore/eventstoredb-ee:lts +# env: +# EVENTSTORE_INSECURE: true +# EVENTSTORE_MEM_DB: false +# EVENTSTORE_RUN_PROJECTIONS: all +# EVENTSTORE_START_STANDARD_PROJECTIONS: true +# ports: +# - 2113:2113 +# options: --health-cmd "exit 0" +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# with: +# ref: ${{ github.event.inputs.ref || github.ref }} +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# 9.0.x +# - name: Compile +# shell: bash +# run: | +# dotnet build samples +# - name: Run +# shell: bash +# run: | +# find samples/ -type f -iname "*.csproj" -print0 | xargs -0L1 dotnet run --framework ${{ matrix.framework }} --project - generate-certificates: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Generate certificates - run: | - mkdir -p certs - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.kurrent.io/eventstore-utils/es-gencert-cli:latest create-ca -out /tmp/ca - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.kurrent.io/eventstore-utils/es-gencert-cli:latest create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.kurrent.io/eventstore-utils/es-gencert-cli:latest create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.kurrent.io/eventstore-utils/es-gencert-cli:latest create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid - - name: Set permissions on certificates - run: | - sudo chown -R $USER:$USER certs - sudo chmod -R 755 certs - - name: Upload certificates - uses: actions/upload-artifact@v4 - with: - name: certs - path: certs +# generate-certificates: +# runs-on: ubuntu-latest +# steps: +# - name: Checkout code +# uses: actions/checkout@v4 +# - name: Generate certificates +# run: | +# mkdir -p certs +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.kurrent.io/eventstore-utils/es-gencert-cli:latest create-ca -out /tmp/ca +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.kurrent.io/eventstore-utils/es-gencert-cli:latest create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.kurrent.io/eventstore-utils/es-gencert-cli:latest create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.kurrent.io/eventstore-utils/es-gencert-cli:latest create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid +# - name: Set permissions on certificates +# run: | +# sudo chown -R $USER:$USER certs +# sudo chmod -R 755 certs +# - name: Upload certificates +# uses: actions/upload-artifact@v4 +# with: +# name: certs +# path: certs - test: - needs: generate-certificates - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - framework: [net8.0, net9.0] - os: [ubuntu-latest] - configuration: [release] - test: - [ - Streams, - PersistentSubscriptions, - Operations, - ProjectionManagement, - UserManagement, - Security, - Misc, - ] - runs-on: ${{ matrix.os }} - name: ${{ matrix.test }} (${{ matrix.os }}, ${{ matrix.framework }}) - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: ${{ github.event.inputs.ref || github.ref }} - - name: Login to Cloudsmith - uses: docker/login-action@v3 - with: - registry: docker.kurrent.io - username: ${{ secrets.CLOUDSMITH_CICD_USER }} - password: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} - - name: Pull EventStore Image - shell: bash - run: | - docker pull docker.kurrent.io/eventstore-ce/eventstoredb-ce:ci - - shell: bash - run: | - git fetch --prune --unshallow - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - 9.0.x - - name: Compile - shell: bash - run: | - dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/KurrentDB.Client - - name: Download certificates - uses: actions/download-artifact@v4 - with: - name: certs - path: certs - - name: Run Tests (Linux) - if: runner.os == 'Linux' - shell: bash - env: - ES_DOCKER_TAG: ci - ES_DOCKER_REGISTRY: docker.kurrent.io/eventstore-ce/eventstoredb-ce - run: | - dotnet test --configuration ${{ matrix.configuration }} --blame \ - --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ - --framework ${{ matrix.framework }} \ - --filter "Category=Target:${{ matrix.test }}" \ - test/KurrentDB.Client.Tests - - name: Run Tests (Windows) - if: runner.os == 'Windows' - shell: pwsh - env: - ES_DOCKER_TAG: ci - ES_DOCKER_REGISTRY: docker.kurrent.io/eventstore-ce/eventstoredb-ce - run: | - dotnet test --configuration ${{ matrix.configuration }} --blame ` - --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" ` - --framework ${{ matrix.framework }} ` - --filter "Category=Target:${{ matrix.test }}" ` - test/KurrentDB.Client.Tests +# test: +# needs: generate-certificates +# timeout-minutes: 10 +# strategy: +# fail-fast: false +# matrix: +# framework: [net8.0, net9.0] +# os: [ubuntu-latest] +# configuration: [release] +# test: +# [ +# Streams, +# PersistentSubscriptions, +# Operations, +# ProjectionManagement, +# UserManagement, +# Security, +# Misc, +# ] +# runs-on: ${{ matrix.os }} +# name: ${{ matrix.test }} (${{ matrix.os }}, ${{ matrix.framework }}) +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# with: +# ref: ${{ github.event.inputs.ref || github.ref }} +# - name: Login to Cloudsmith +# uses: docker/login-action@v3 +# with: +# registry: docker.kurrent.io +# username: ${{ secrets.CLOUDSMITH_CICD_USER }} +# password: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} +# - name: Pull EventStore Image +# shell: bash +# run: | +# docker pull docker.kurrent.io/eventstore-ce/eventstoredb-ce:ci +# - shell: bash +# run: | +# git fetch --prune --unshallow +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# 9.0.x +# - name: Compile +# shell: bash +# run: | +# dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/KurrentDB.Client +# - name: Download certificates +# uses: actions/download-artifact@v4 +# with: +# name: certs +# path: certs +# - name: Run Tests (Linux) +# if: runner.os == 'Linux' +# shell: bash +# env: +# ES_DOCKER_TAG: ci +# ES_DOCKER_REGISTRY: docker.kurrent.io/eventstore-ce/eventstoredb-ce +# run: | +# dotnet test --configuration ${{ matrix.configuration }} --blame \ +# --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ +# --framework ${{ matrix.framework }} \ +# --filter "Category=Target:${{ matrix.test }}" \ +# test/KurrentDB.Client.Tests +# - name: Run Tests (Windows) +# if: runner.os == 'Windows' +# shell: pwsh +# env: +# ES_DOCKER_TAG: ci +# ES_DOCKER_REGISTRY: docker.kurrent.io/eventstore-ce/eventstoredb-ce +# run: | +# dotnet test --configuration ${{ matrix.configuration }} --blame ` +# --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" ` +# --framework ${{ matrix.framework }} ` +# --filter "Category=Target:${{ matrix.test }}" ` +# test/KurrentDB.Client.Tests - publish: - timeout-minutes: 5 - needs: [vulnerability-scan, test, build-samples] - runs-on: ubuntu-latest - name: publish - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: ${{ github.event.inputs.ref || github.ref }} - fetch-depth: 0 +# publish: +# timeout-minutes: 5 +# needs: [vulnerability-scan, test, build-samples] +# runs-on: ubuntu-latest +# name: publish +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# with: +# ref: ${{ github.event.inputs.ref || github.ref }} +# fetch-depth: 0 - - name: Get Version - id: get_version - run: | - echo "branch=${GITHUB_REF:10}" >> $GITHUB_OUTPUT - dotnet nuget list source - dotnet tool restore +# - name: Get Version +# id: get_version +# run: | +# echo "branch=${GITHUB_REF:10}" >> $GITHUB_OUTPUT +# dotnet nuget list source +# dotnet tool restore - if [ -n "${{ github.event.inputs.version }}" ]; then - version="${{ github.event.inputs.version }}" - else - version=$(dotnet tool run minver -- --tag-prefix=v) - fi +# if [ -n "${{ github.event.inputs.version }}" ]; then +# version="${{ github.event.inputs.version }}" +# else +# version=$(dotnet tool run minver -- --tag-prefix=v) +# fi - echo "version=${version}" >> $GITHUB_OUTPUT +# echo "version=${version}" >> $GITHUB_OUTPUT - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - 9.0.x +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# 9.0.x - - name: Dotnet Pack - shell: bash - run: | - echo "version=${{ steps.get_version.outputs.version }}" - mkdir -p packages +# - name: Dotnet Pack +# shell: bash +# run: | +# echo "version=${{ steps.get_version.outputs.version }}" +# mkdir -p packages - extra_minver_flag="" - if [ -n "${{ github.event.inputs.version }}" ]; then - extra_minver_flag="/p:MinVerSkip=true" - fi +# extra_minver_flag="" +# if [ -n "${{ github.event.inputs.version }}" ]; then +# extra_minver_flag="/p:MinVerSkip=true" +# fi - dotnet pack /p:Version=${{ steps.get_version.outputs.version }} $extra_minver_flag \ - --configuration=Release \ - /p:PublishDir=./packages \ - /p:NoWarn=NU5105 \ - /p:RepositoryUrl=https://github.com/kurrent-io/EventStore-Client-Dotnet \ - /p:RepositoryType=git +# dotnet pack /p:Version=${{ steps.get_version.outputs.version }} $extra_minver_flag \ +# --configuration=Release \ +# /p:PublishDir=./packages \ +# /p:NoWarn=NU5105 \ +# /p:RepositoryUrl=https://github.com/kurrent-io/EventStore-Client-Dotnet \ +# /p:RepositoryType=git - - name: Publish Artifacts - uses: actions/upload-artifact@v4 - with: - path: packages - name: nuget-packages +# - name: Publish Artifacts +# uses: actions/upload-artifact@v4 +# with: +# path: packages +# name: nuget-packages - - name: Dotnet Push to Github Packages - shell: bash - if: github.event.inputs.dry_run != 'true' || github.event_name == 'push' - run: | - dotnet tool restore - find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.GITHUB_TOKEN }} --source https://nuget.pkg.github.com/kurrent-io/index.json --skip-duplicate +# - name: Dotnet Push to Github Packages +# shell: bash +# if: github.event.inputs.dry_run != 'true' || github.event_name == 'push' +# run: | +# dotnet tool restore +# find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.GITHUB_TOKEN }} --source https://nuget.pkg.github.com/kurrent-io/index.json --skip-duplicate - - name: Dotnet Push to Nuget.org - shell: bash - if: github.event.inputs.dry_run != 'true' || github.event_name == 'push' - run: | - dotnet nuget list source - dotnet tool restore - find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.KURRENT_NUGET_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate +# - name: Dotnet Push to Nuget.org +# shell: bash +# if: github.event.inputs.dry_run != 'true' || github.event_name == 'push' +# run: | +# dotnet nuget list source +# dotnet tool restore +# find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.KURRENT_NUGET_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..472ce65de --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,83 @@ +name: Test + +on: + workflow_call: + secrets: + CLOUDSMITH_CICD_USER: + required: false + CLOUDSMITH_CICD_TOKEN: + required: false + inputs: + runtime: + required: true + type: string + + registry: + description: "The Docker registry to use" + type: string + required: false + + image: + description: "The Docker image to use" + type: string + required: false + + tag: + description: "The Docker image tag to use" + type: string + required: false + +jobs: + load_configuration: + uses: ./.github/workflows/load-configuration.yml + with: + runtime: ${{ inputs.runtime }} + registry: ${{ inputs.registry }} + image: ${{ inputs.image }} + tag: ${{ inputs.tag }} + + test: + name: Test + needs: load_configuration + timeout-minutes: 10 + + strategy: + fail-fast: false + matrix: + framework: [ net8.0, net9.0 ] + test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement, Security, Misc ] + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Login to Cloudsmith + uses: docker/login-action@v3 + with: + registry: docker.kurrent.io + username: ${{ secrets.CLOUDSMITH_CICD_USER }} + password: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} + + - name: Install dotnet SDKs + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x + 9.0.x + + - name: Run Tests + shell: bash + env: + TESTCONTAINER_KURRENTDB_IMAGE: "${{ needs.load_configuration.outputs.registry }}/${{ needs.load_configuration.outputs.image }}:${{ needs.load_configuration.outputs.tag }}" + run: | + sudo ./gencert.sh + dotnet test --configuration release --blame \ + --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ + --framework ${{ matrix.framework }} \ + --filter "Category=Target:${{ matrix.test }}" \ + test/KurrentDB.Client.Tests + From 5251b8b16e1648d139a65a30155c53e8e2bbf42b Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 6 Aug 2025 17:42:14 +0400 Subject: [PATCH 06/29] Add v2 protos and fix CI --- .../Core/GrpcServerCapabilitiesClient.cs | 4 +- .../kurrentdb/protocol/v1}/code.proto | 0 .../kurrentdb/protocol/v1}/gossip.proto | 0 .../kurrentdb/protocol/v1}/operations.proto | 4 + .../v1}/persistentsubscriptions.proto | 6 +- .../protocol/v1}/projectionmanagement.proto | 4 + .../protocol/v1}/serverfeatures.proto | 7 +- .../kurrentdb/protocol/v1}/shared.proto | 2 +- .../kurrentdb/protocol/v1}/status.proto | 0 .../proto/kurrentdb/protocol/v1/streams.proto | 357 +++++++++++++ .../protocol/v1}/usermanagement.proto | 4 + .../proto/kurrentdb/protocol/v2/core.proto | 129 +++++ .../protocol/v2/streams/error_details.proto | 88 ++++ .../protocol/v2/streams/streams.proto | 481 ++++++++++++++++++ .../protocol/v2/streams/streams_manage.proto | 123 +++++ .../protocol/v2/streams/streams_read.proto | 200 ++++++++ .../Core/protos/streams.proto | 316 ------------ src/KurrentDB.Client/KurrentDB.Client.csproj | 88 +--- .../KurrentDBOperationsClient.Admin.cs | 13 +- .../KurrentDBOperationsClient.Scavenge.cs | 7 +- ...tDBPersistentSubscriptionsClient.Create.cs | 7 +- ...tDBPersistentSubscriptionsClient.Delete.cs | 5 +- ...entDBPersistentSubscriptionsClient.Info.cs | 5 +- ...entDBPersistentSubscriptionsClient.List.cs | 5 +- ...entDBPersistentSubscriptionsClient.Read.cs | 7 +- ...sistentSubscriptionsClient.ReplayParked.cs | 5 +- ...entSubscriptionsClient.RestartSubsystem.cs | 4 +- ...tDBPersistentSubscriptionsClient.Update.cs | 5 +- .../PersistentSubscriptionInfo.cs | 2 +- ...entDBProjectionManagementClient.Control.cs | 11 +- ...rentDBProjectionManagementClient.Create.cs | 9 +- ...rrentDBProjectionManagementClient.State.cs | 7 +- ...DBProjectionManagementClient.Statistics.cs | 5 +- ...rentDBProjectionManagementClient.Update.cs | 5 +- .../Streams/KurrentDBClient.Append.cs | 7 +- .../Streams/KurrentDBClient.Delete.cs | 5 +- .../Streams/KurrentDBClient.Metadata.cs | 2 +- .../Streams/KurrentDBClient.Read.cs | 9 +- .../Streams/KurrentDBClient.Subscriptions.cs | 7 +- .../Streams/KurrentDBClient.Tombstone.cs | 5 +- .../Streams/KurrentDBClient.cs | 2 +- .../Streams/Streams/AppendReq.cs | 27 +- .../Streams/Streams/BatchAppendReq.cs | 3 +- .../Streams/Streams/BatchAppendResp.cs | 5 +- .../Streams/Streams/DeleteReq.cs | 3 +- .../Streams/Streams/ReadReq.cs | 4 +- .../Streams/Streams/TombstoneReq.cs | 3 +- .../KurrentDBUserManagementClient.cs | 17 +- .../Fixtures/BaseTestNode.cs | 162 ------ .../Fixtures/KurrentDBPermanentFixture.cs | 93 +--- .../Fixtures/KurrentDBPermanentTestNode.cs | 200 +------- .../Fixtures/KurrentDBTemporaryFixture.cs | 67 +-- .../Fixtures/KurrentDBTemporaryTestNode.cs | 163 +----- .../FluentDocker/TestContainerService.cs | 136 ++++- .../GlobalEnvironment.cs | 29 +- .../KurrentDB.Client.Tests.Common.csproj | 3 - test/KurrentDB.Client.Tests.Common/shared.env | 15 - .../Streams/AppendTests.cs | 12 +- .../Streams/Read/ReadStreamBackwardTests.cs | 4 +- .../Streams/Read/ReadStreamForwardTests.cs | 12 +- .../UserManagement/ListUserTests.cs | 23 +- 61 files changed, 1783 insertions(+), 1150 deletions(-) rename src/KurrentDB.Client/Core/{protos => proto/kurrentdb/protocol/v1}/code.proto (100%) rename src/KurrentDB.Client/Core/{protos => proto/kurrentdb/protocol/v1}/gossip.proto (100%) rename src/KurrentDB.Client/Core/{protos => proto/kurrentdb/protocol/v1}/operations.proto (93%) rename src/KurrentDB.Client/Core/{protos => proto/kurrentdb/protocol/v1}/persistentsubscriptions.proto (98%) rename src/KurrentDB.Client/Core/{protos => proto/kurrentdb/protocol/v1}/projectionmanagement.proto (97%) rename src/KurrentDB.Client/Core/{protos => proto/kurrentdb/protocol/v1}/serverfeatures.proto (77%) rename src/KurrentDB.Client/Core/{protos => proto/kurrentdb/protocol/v1}/shared.proto (95%) rename src/KurrentDB.Client/Core/{protos => proto/kurrentdb/protocol/v1}/status.proto (100%) create mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/streams.proto rename src/KurrentDB.Client/Core/{protos => proto/kurrentdb/protocol/v1}/usermanagement.proto (97%) create mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/core.proto create mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/error_details.proto create mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto create mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_manage.proto create mode 100644 src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_read.proto delete mode 100644 src/KurrentDB.Client/Core/protos/streams.proto delete mode 100644 test/KurrentDB.Client.Tests.Common/Fixtures/BaseTestNode.cs delete mode 100644 test/KurrentDB.Client.Tests.Common/shared.env diff --git a/src/KurrentDB.Client/Core/GrpcServerCapabilitiesClient.cs b/src/KurrentDB.Client/Core/GrpcServerCapabilitiesClient.cs index e93ee2be7..ee56e9527 100644 --- a/src/KurrentDB.Client/Core/GrpcServerCapabilitiesClient.cs +++ b/src/KurrentDB.Client/Core/GrpcServerCapabilitiesClient.cs @@ -1,5 +1,5 @@ -using EventStore.Client.ServerFeatures; using Grpc.Core; +using static KurrentDB.Protocol.Users.V1.ServerFeatures; namespace KurrentDB.Client { internal class GrpcServerCapabilitiesClient : IServerCapabilitiesClient { @@ -13,7 +13,7 @@ public async Task GetAsync( CallInvoker callInvoker, CancellationToken cancellationToken) { - var client = new ServerFeatures.ServerFeaturesClient(callInvoker); + var client = new ServerFeaturesClient(callInvoker); using var call = client.GetSupportedMethodsAsync( new(), KurrentDBCallOptions.CreateNonStreaming( diff --git a/src/KurrentDB.Client/Core/protos/code.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/code.proto similarity index 100% rename from src/KurrentDB.Client/Core/protos/code.proto rename to src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/code.proto diff --git a/src/KurrentDB.Client/Core/protos/gossip.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/gossip.proto similarity index 100% rename from src/KurrentDB.Client/Core/protos/gossip.proto rename to src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/gossip.proto diff --git a/src/KurrentDB.Client/Core/protos/operations.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/operations.proto similarity index 93% rename from src/KurrentDB.Client/Core/protos/operations.proto rename to src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/operations.proto index f4f9ae3c3..9d0f654bf 100644 --- a/src/KurrentDB.Client/Core/protos/operations.proto +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/operations.proto @@ -1,5 +1,9 @@ syntax = "proto3"; + package event_store.client.operations; + +option csharp_namespace = "KurrentDB.Protocol.Operations.V1"; + option java_package = "io.kurrent.client.operations"; import "shared.proto"; diff --git a/src/KurrentDB.Client/Core/protos/persistentsubscriptions.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/persistentsubscriptions.proto similarity index 98% rename from src/KurrentDB.Client/Core/protos/persistentsubscriptions.proto rename to src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/persistentsubscriptions.proto index ac63a3a6d..a1e9b55b8 100644 --- a/src/KurrentDB.Client/Core/protos/persistentsubscriptions.proto +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/persistentsubscriptions.proto @@ -1,6 +1,10 @@ syntax = "proto3"; + package event_store.client.persistent_subscriptions; -option java_package = "io.kurrent.dbclient.proto.persistentsubscriptions"; + +option csharp_namespace = "KurrentDB.Protocol.PersistentSubscriptions.V1"; + +option java_package = "io.kurrent.client.persistentsubscriptions"; import "shared.proto"; diff --git a/src/KurrentDB.Client/Core/protos/projectionmanagement.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/projectionmanagement.proto similarity index 97% rename from src/KurrentDB.Client/Core/protos/projectionmanagement.proto rename to src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/projectionmanagement.proto index f16877012..1c832ab9b 100644 --- a/src/KurrentDB.Client/Core/protos/projectionmanagement.proto +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/projectionmanagement.proto @@ -1,5 +1,9 @@ syntax = "proto3"; + package event_store.client.projections; + +option csharp_namespace = "KurrentDB.Protocol.Projections.V1"; + option java_package = "io.kurrent.client.projections"; import "google/protobuf/struct.proto"; diff --git a/src/KurrentDB.Client/Core/protos/serverfeatures.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/serverfeatures.proto similarity index 77% rename from src/KurrentDB.Client/Core/protos/serverfeatures.proto rename to src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/serverfeatures.proto index 61c4ab773..2bec769c0 100644 --- a/src/KurrentDB.Client/Core/protos/serverfeatures.proto +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/serverfeatures.proto @@ -1,6 +1,11 @@ syntax = "proto3"; + package event_store.client.server_features; -option java_package = "io.kurrent.dbclient.proto.serverfeatures"; + +option csharp_namespace = "KurrentDB.Protocol.Users.V1"; + +option java_package = "io.kurrent.client.serverfeatures"; + import "shared.proto"; service ServerFeatures { diff --git a/src/KurrentDB.Client/Core/protos/shared.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/shared.proto similarity index 95% rename from src/KurrentDB.Client/Core/protos/shared.proto rename to src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/shared.proto index 24780afc3..a785b7745 100644 --- a/src/KurrentDB.Client/Core/protos/shared.proto +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/shared.proto @@ -1,6 +1,6 @@ syntax = "proto3"; package event_store.client; -option java_package = "io.kurrent.dbclient.proto.shared"; +option java_package = "io.kurrent.client.proto.shared"; import "google/protobuf/empty.proto"; message UUID { diff --git a/src/KurrentDB.Client/Core/protos/status.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/status.proto similarity index 100% rename from src/KurrentDB.Client/Core/protos/status.proto rename to src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/status.proto diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/streams.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/streams.proto new file mode 100644 index 000000000..3e2053f8b --- /dev/null +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/streams.proto @@ -0,0 +1,357 @@ +syntax = "proto3"; + +package event_store.client.streams; + +option csharp_namespace = "KurrentDB.Protocol.Streams.V1"; + +option java_package = "com.eventstore.dbclient.proto.streams"; + +import "shared.proto"; +import "status.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; + +service Streams { + rpc Read (ReadReq) returns (stream ReadResp); + rpc Append (stream AppendReq) returns (AppendResp); + rpc Delete (DeleteReq) returns (DeleteResp); + rpc Tombstone (TombstoneReq) returns (TombstoneResp); + rpc BatchAppend (stream BatchAppendReq) returns (stream BatchAppendResp); +} + +message ReadReq { + Options options = 1; + + message Options { + oneof stream_option { + StreamOptions stream = 1; + AllOptions all = 2; + } + ReadDirection read_direction = 3; + bool resolve_links = 4; + oneof count_option { + uint64 count = 5; + SubscriptionOptions subscription = 6; + } + oneof filter_option { + FilterOptions filter = 7; + event_store.client.Empty no_filter = 8; + } + UUIDOption uuid_option = 9; + ControlOption control_option = 10; + + enum ReadDirection { + Forwards = 0; + Backwards = 1; + } + message StreamOptions { + event_store.client.StreamIdentifier stream_identifier = 1; + oneof revision_option { + uint64 revision = 2; + event_store.client.Empty start = 3; + event_store.client.Empty end = 4; + } + } + message AllOptions { + oneof all_option { + Position position = 1; + event_store.client.Empty start = 2; + event_store.client.Empty end = 3; + } + } + message SubscriptionOptions { + } + message Position { + uint64 commit_position = 1; + uint64 prepare_position = 2; + } + message FilterOptions { + oneof filter { + Expression stream_identifier = 1; + Expression event_type = 2; + } + oneof window { + uint32 max = 3; + event_store.client.Empty count = 4; + } + uint32 checkpointIntervalMultiplier = 5; + + message Expression { + string regex = 1; + repeated string prefix = 2; + } + } + message UUIDOption { + oneof content { + event_store.client.Empty structured = 1; + event_store.client.Empty string = 2; + } + } + message ControlOption { + uint32 compatibility = 1; + } + } +} + +message ReadResp { + oneof content { + ReadEvent event = 1; + SubscriptionConfirmation confirmation = 2; + Checkpoint checkpoint = 3; + StreamNotFound stream_not_found = 4; + uint64 first_stream_position = 5; + uint64 last_stream_position = 6; + AllStreamPosition last_all_stream_position = 7; + CaughtUp caught_up = 8; + FellBehind fell_behind = 9; + } + + // The $all or stream subscription has caught up and become live. + message CaughtUp { + // Current time in the server when the subscription caught up + google.protobuf.Timestamp timestamp = 1; + + // Checkpoint for resuming a stream subscription. + // For stream subscriptions it is populated unless the stream is empty. + // For $all subscriptions it is not populated. + optional int64 stream_revision = 2; + + // Checkpoint for resuming a $all subscription. + // For stream subscriptions it is not populated. + // For $all subscriptions it is populated unless the database is empty. + optional Position position = 3; + } + + // The $all or stream subscription has fallen back into catchup mode and is no longer live. + message FellBehind { + // Current time in the server when the subscription fell behind + google.protobuf.Timestamp timestamp = 1; + + // Checkpoint for resuming a stream subscription. + // For stream subscriptions it is populated unless the stream is empty. + // For $all subscriptions it is not populated. + optional int64 stream_revision = 2; + + // Checkpoint for resuming a $all subscription. + // For stream subscriptions it is not populated. + // For $all subscriptions it is populated unless the database is empty. + optional Position position = 3; + } + + message ReadEvent { + RecordedEvent event = 1; + RecordedEvent link = 2; + oneof position { + uint64 commit_position = 3; + event_store.client.Empty no_position = 4; + } + + message RecordedEvent { + event_store.client.UUID id = 1; + event_store.client.StreamIdentifier stream_identifier = 2; + uint64 stream_revision = 3; + uint64 prepare_position = 4; + uint64 commit_position = 5; + map metadata = 6; + bytes custom_metadata = 7; + bytes data = 8; + } + } + message SubscriptionConfirmation { + string subscription_id = 1; + } + message Checkpoint { + uint64 commit_position = 1; + uint64 prepare_position = 2; + + // Current time in the server when the checkpoint was reached + google.protobuf.Timestamp timestamp = 3; + } + + message Position { + uint64 commit_position = 1; + uint64 prepare_position = 2; + } + + message StreamNotFound { + event_store.client.StreamIdentifier stream_identifier = 1; + } +} + +message AppendReq { + oneof content { + Options options = 1; + ProposedMessage proposed_message = 2; + } + + message Options { + event_store.client.StreamIdentifier stream_identifier = 1; + oneof expected_stream_revision { + uint64 revision = 2; + event_store.client.Empty no_stream = 3; + event_store.client.Empty any = 4; + event_store.client.Empty stream_exists = 5; + } + } + message ProposedMessage { + event_store.client.UUID id = 1; + map metadata = 2; + bytes custom_metadata = 3; + bytes data = 4; + } +} + +message AppendResp { + oneof result { + Success success = 1; + WrongExpectedVersion wrong_expected_version = 2; + } + + message Position { + uint64 commit_position = 1; + uint64 prepare_position = 2; + } + + message Success { + oneof current_revision_option { + uint64 current_revision = 1; + event_store.client.Empty no_stream = 2; + } + oneof position_option { + Position position = 3; + event_store.client.Empty no_position = 4; + } + } + + message WrongExpectedVersion { + oneof current_revision_option_20_6_0 { + uint64 current_revision_20_6_0 = 1; + event_store.client.Empty no_stream_20_6_0 = 2; + } + oneof expected_revision_option_20_6_0 { + uint64 expected_revision_20_6_0 = 3; + event_store.client.Empty any_20_6_0 = 4; + event_store.client.Empty stream_exists_20_6_0 = 5; + } + oneof current_revision_option { + uint64 current_revision = 6; + event_store.client.Empty current_no_stream = 7; + } + oneof expected_revision_option { + uint64 expected_revision = 8; + event_store.client.Empty expected_any = 9; + event_store.client.Empty expected_stream_exists = 10; + event_store.client.Empty expected_no_stream = 11; + } + + } +} + +message BatchAppendReq { + event_store.client.UUID correlation_id = 1; + Options options = 2; + repeated ProposedMessage proposed_messages = 3; + bool is_final = 4; + + message Options { + event_store.client.StreamIdentifier stream_identifier = 1; + oneof expected_stream_position { + uint64 stream_position = 2; + google.protobuf.Empty no_stream = 3; + google.protobuf.Empty any = 4; + google.protobuf.Empty stream_exists = 5; + } + oneof deadline_option { + google.protobuf.Timestamp deadline_21_10_0 = 6; + google.protobuf.Duration deadline = 7; + } + } + + message ProposedMessage { + event_store.client.UUID id = 1; + map metadata = 2; + bytes custom_metadata = 3; + bytes data = 4; + } +} + +message BatchAppendResp { + event_store.client.UUID correlation_id = 1; + oneof result { + google.rpc.Status error = 2; + Success success = 3; + } + + event_store.client.StreamIdentifier stream_identifier = 4; + + oneof expected_stream_position { + uint64 stream_position = 5; + google.protobuf.Empty no_stream = 6; + google.protobuf.Empty any = 7; + google.protobuf.Empty stream_exists = 8; + } + + message Success { + oneof current_revision_option { + uint64 current_revision = 1; + google.protobuf.Empty no_stream = 2; + } + oneof position_option { + event_store.client.AllStreamPosition position = 3; + google.protobuf.Empty no_position = 4; + } + } +} + +message DeleteReq { + Options options = 1; + + message Options { + event_store.client.StreamIdentifier stream_identifier = 1; + oneof expected_stream_revision { + uint64 revision = 2; + event_store.client.Empty no_stream = 3; + event_store.client.Empty any = 4; + event_store.client.Empty stream_exists = 5; + } + } +} + +message DeleteResp { + oneof position_option { + Position position = 1; + event_store.client.Empty no_position = 2; + } + + message Position { + uint64 commit_position = 1; + uint64 prepare_position = 2; + } +} + +message TombstoneReq { + Options options = 1; + + message Options { + event_store.client.StreamIdentifier stream_identifier = 1; + oneof expected_stream_revision { + uint64 revision = 2; + event_store.client.Empty no_stream = 3; + event_store.client.Empty any = 4; + event_store.client.Empty stream_exists = 5; + } + } +} + +message TombstoneResp { + oneof position_option { + Position position = 1; + event_store.client.Empty no_position = 2; + } + + message Position { + uint64 commit_position = 1; + uint64 prepare_position = 2; + } +} diff --git a/src/KurrentDB.Client/Core/protos/usermanagement.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/usermanagement.proto similarity index 97% rename from src/KurrentDB.Client/Core/protos/usermanagement.proto rename to src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/usermanagement.proto index 4b55251fd..85b465b32 100644 --- a/src/KurrentDB.Client/Core/protos/usermanagement.proto +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v1/usermanagement.proto @@ -1,5 +1,9 @@ syntax = "proto3"; + package event_store.client.users; + +option csharp_namespace = "KurrentDB.Protocol.Users.V1"; + option java_package = "io.kurrent.client.users"; service Users { diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/core.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/core.proto new file mode 100644 index 000000000..d22daf893 --- /dev/null +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/core.proto @@ -0,0 +1,129 @@ +syntax = "proto3"; + +package kurrentdb.protocol; + +option csharp_namespace = "KurrentDB.Protocol"; +option java_package = "io.kurrentdb.protocol"; +option java_multiple_files = true; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/descriptor.proto"; + +//=================================================================== +// Dynamic values +//=================================================================== + +// Represents a list of dynamically typed values. +message DynamicValueList { + // Repeated property of dynamically typed values. + repeated DynamicValue values = 1; +} + +// Represents a map of dynamically typed values. +message DynamicValueMap { + // A map of string keys to dynamically typed values. + map values = 1; +} + +// Represents a dynamic value +message DynamicValue { + oneof kind { + // Represents a null value. + google.protobuf.NullValue null_value = 1; + // Represents a 32-bit signed integer value. + sint32 int32_value = 2; + // Represents a 64-bit signed integer value. + sint64 int64_value = 3; + // Represents a byte array value. + bytes bytes_value = 4; + // Represents a 64-bit double-precision floating-point value. + double double_value = 5; + // Represents a 32-bit single-precision floating-point value + float float_value = 6; + // Represents a string value. + string string_value = 7; + // Represents a boolean value. + bool boolean_value = 8; + // Represents a timestamp value. + google.protobuf.Timestamp timestamp_value = 9; + // Represents a duration value. + google.protobuf.Duration duration_value = 10; + } +} + +//=================================================================== +// Error Annotations +//=================================================================== + +message ErrorAnnotations { + // Identifies the error condition. + string code = 1; + + // Severity of the error. + Severity severity = 2; + + // Human-readable message that describes the error condition. + optional string message = 3; + + enum Severity { + // The error is recoverable, the operation failed but the session can continue. + RECOVERABLE = 0; + // The error is fatal and the session should be terminated. + FATAL = 1; + // The error is retriable, the operation failed but can be retried. + RETRIABLE = 2; + } +} + +// Extend the MessageOptions to include error information. +extend google.protobuf.MessageOptions { + // Provides additional information about the error condition. + optional ErrorAnnotations error_info = 50000; +} + +//=================================================================== +// Error Details +//=================================================================== + +// Provides detailed information about specific error conditions. +message CoreErrorDetails { + // When the user does not have sufficient permissions to perform the + // operation. + message AccessDenied { + option (error_info) = { + code : "ACCESS_DENIED", + severity : RECOVERABLE, + message : "The user does not have sufficient permissions to perform the operation. Please check your permissions and try again." + }; + } + + // When the user does not have sufficient permissions to perform the + // operation. + message DeadlineExceeded { + option (error_info) = { + code : "DEADLINE_EXCEEDED", + severity : RECOVERABLE, + message : "The operation has timed out and exceeded the deadline. Please try again later." + }; + } + + // When the user is not authenticated. + message NotAuthenticated { + option (error_info) = { + code : "NOT_AUTHENTICATED", + severity : RECOVERABLE, + message : "The user is not authenticated. Please provide valid authentication credentials." + }; + } + + // When the user is not found. + message UserNotFound { + option (error_info) = { + code : "USER_NOT_FOUND", + severity : RECOVERABLE, + message : "The specified user was not found. Please check the user ID and try again." + }; + } +} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/error_details.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/error_details.proto new file mode 100644 index 000000000..4c38af4f3 --- /dev/null +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/error_details.proto @@ -0,0 +1,88 @@ +syntax = "proto3"; + +package kurrentdb.protocol.v2; + +option csharp_namespace = "KurrentDB.Protocol.Streams.V2"; +option java_package = "io.kurrentdb.protocol.streams.v2"; +option java_multiple_files = true; + +import "google/protobuf/timestamp.proto"; +import "core.proto"; + +message StreamsErrorDetails { + // When the stream has been deleted. + message StreamDeleted { + option (error_info) = { + code : "STREAM_DELETED", + severity : RECOVERABLE, + message : "The stream has been soft deleted. It will not be visible in the stream list, until it is restored by appending to it again." + }; + + // The name of the stream that was deleted. + optional string stream = 1; + + // The time when the stream was deleted. + google.protobuf.Timestamp deleted_at = 2; + } + + // When the stream has been tombstoned. + message StreamTombstoned { + option (error_info) = { + code : "STREAM_TOMBSTONED", + severity : FATAL, + message : "The stream has been tombstoned. It has been permanently removed from the system and cannot be restored." + }; + + // The name of the stream that was tombstoned. + optional string stream = 1; + + // The time when the stream was tombstoned. + google.protobuf.Timestamp tombstoned_at = 2; + } + + // When the stream is not found. + message StreamNotFound { + option (error_info) = { + code : "STREAM_NOT_FOUND", + severity : RECOVERABLE, + message : "The specified stream was not found. Please check the stream name and try again." + }; + + // The name of the stream that was not found. + optional string stream = 1; + } + + // When the expected revision of the stream does not match the actual + // revision. + message StreamRevisionConflict { + option (error_info) = { + code : "REVISION_CONFLICT", + severity : RECOVERABLE, + message : "The actual stream revision does not match the expected revision." + }; + + // The actual revision of the stream. + int64 stream_revision = 1; + } + + // When the transaction exceeds the maximum size allowed + // (its bigger than the configured chunk size). + message TransactionMaxSizeExceeded { + option (error_info) = { + code : "TRANSACTION_MAX_SIZE_EXCEEDED", + severity : FATAL, + message : "The transaction exceeds the maximum size allowed." + }; + + // The maximum allowed size of the transaction. + uint32 max_size = 1; + } + + message LogPositionNotFound { + option (error_info) = { + code : "LOG_POSITION_NOT_FOUND", + severity : RECOVERABLE, + message : "The specified log position was not found." + }; + } +} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto new file mode 100644 index 000000000..1c12abf63 --- /dev/null +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams.proto @@ -0,0 +1,481 @@ +syntax = "proto3"; + +package kurrentdb.protocol.v2; + +option csharp_namespace = "KurrentDB.Protocol.Streams.V2"; +option java_package = "io.kurrentdb.protocol.streams.v2"; +option java_multiple_files = true; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/descriptor.proto"; + +import "streams/error_details.proto"; +import "core.proto"; + +service StreamsService { + // Executes an atomic operation to append records to multiple streams. + // This transactional method ensures that all appends either succeed + // completely, or are entirely rolled back, thereby maintaining strict data + // consistency across all involved streams. + rpc MultiStreamAppend(MultiStreamAppendRequest) returns (MultiStreamAppendResponse); + + // Streaming version of MultiStreamAppend that allows clients to send multiple + // append requests over a single connection. When the stream completes, all + // records are appended transactionally (all succeed or fail together). + // Provides improved efficiency for high-throughput scenarios while + // maintaining the same transactional guarantees. + rpc MultiStreamAppendSession(stream AppendStreamRequest) returns (MultiStreamAppendResponse); + +// // Appends records to a specific stream. +// rpc AppendStream(AppendStreamRequest) returns (AppendStreamResponse); + +// // Append batches of records to a stream continuously, while guaranteeing pipelined +// // requests are processed in order. If any request fails, the session is terminated. +// rpc AppendStreamSession(stream AppendStreamRequest) returns (stream AppendStreamResponse); + +// // Retrieve a batch of records +// rpc ReadStream(ReadRequest) returns (ReadResponse); + +// // Retrieve batches of records continuously. +// rpc ReadSession(ReadRequest) returns (stream ReadResponse); +} + +//=================================================================== +// Append Operations +//=================================================================== + +// Record to be appended to a stream. +message AppendRecord { + // Universally Unique identifier for the record. + // If not provided, the server will generate a new one. + optional string record_id = 1; + +// // The name of the stream to append the record to. +// optional string stream = 6; +// +// // The name of the schema in the registry that defines the structure of the record. +// string schema_name = 4; +// +// // The format of the data in the record. +// SchemaDataFormat data_format = 5; + + // A collection of properties providing additional information about the + // record. This can include user-defined metadata or system properties. + // System properties are prefixed with "$." to avoid conflicts with user-defined properties. + // For example, "$schema.name" or "$schema.data-format". + map properties = 2; + + // The actual data payload of the record, stored as bytes. + bytes data = 3; +} + +// Constants that match the expected state of a stream during an +// append operation. It can be used to specify whether the stream should exist, +// not exist, or can be in any state. +enum ExpectedRevisionConstants { + // The stream should exist and the expected revision should match the current + EXPECTED_REVISION_CONSTANTS_SINGLE_EVENT = 0; + // It is not important whether the stream exists or not. + EXPECTED_REVISION_CONSTANTS_ANY = -2; + // The stream should not exist. If it does, the append will fail. + EXPECTED_REVISION_CONSTANTS_NO_STREAM = -1; + // The stream should exist + EXPECTED_REVISION_CONSTANTS_EXISTS = -4; +} + +// Represents the input for appending records to a specific stream. +message AppendStreamRequest { + // The name of the stream to append records to. + string stream = 1; + // The records to append to the stream. + repeated AppendRecord records = 2; + // The expected revision of the stream. If the stream's current revision does + // not match, the append will fail. + // The expected revision can also be one of the special values + // from ExpectedRevisionConstants. + // Missing value means no expectation, the same as EXPECTED_REVISION_CONSTANTS_ANY + optional sint64 expected_revision = 3; +} + +// Success represents the successful outcome of an append operation. +message AppendStreamSuccess { + // The name of the stream to which records were appended. + string stream = 1; + // The position of the last appended record in the stream. + int64 position = 2; + // The expected revision of the stream after the append operation. + int64 stream_revision = 3; +} + +// Failure represents the detailed error information when an append operation fails. +message AppendStreamFailure { + // The name of the stream to which records were appended. + string stream = 1; + + // The error details + oneof error { + // Failed because the actual stream revision didn't match the expected revision. + StreamsErrorDetails.StreamRevisionConflict stream_revision_conflict = 2; + // Failed because the client lacks sufficient permissions. + CoreErrorDetails.AccessDenied access_denied = 3; + // Failed because the target stream has been deleted. + StreamsErrorDetails.StreamDeleted stream_deleted = 4; + // Failed because the stream was not found. + StreamsErrorDetails.StreamNotFound stream_not_found = 5; + // Failed because the transaction exceeded the maximum size allowed + StreamsErrorDetails.TransactionMaxSizeExceeded transaction_max_size_exceeded = 6; + } +} + +// AppendStreamResponse represents the output of appending records to a specific +// stream. +message AppendStreamResponse { + // The result of the append operation. + oneof result { + // Success represents the successful outcome of an append operation. + AppendStreamSuccess success = 1; + // Failure represents the details of a failed append operation. + AppendStreamFailure failure = 2; + } +} + +// MultiStreamAppendRequest represents a request to append records to multiple streams. +message MultiStreamAppendRequest { + // A list of AppendStreamInput messages, each representing a stream to which records should be appended. + repeated AppendStreamRequest input = 1; +} + +// Response from the MultiStreamAppend operation. +message MultiStreamAppendResponse { + oneof result { + // Success represents the successful outcome of a multi-stream append operation. + Success success = 1; + // Failure represents the details of a failed multi-stream append operation. + Failure failure = 2; + } + + message Success { + repeated AppendStreamSuccess output = 1; + } + + message Failure { + repeated AppendStreamFailure output = 1; + } +} + +////=================================================================== +//// Read Operations +////=================================================================== +// +//// The scope of the read filter determines where the filter will be applied. +//enum ReadFilterScope { +// READ_FILTER_SCOPE_UNSPECIFIED = 0; +// // The filter will be applied to the record stream name +// READ_FILTER_SCOPE_STREAM = 1; +// // The filter will be applied to the record schema name +// READ_FILTER_SCOPE_SCHEMA_NAME = 2; +// // The filter will be applied to the properties of the record +// READ_FILTER_SCOPE_PROPERTIES = 3; +// // The filter will be applied to all the record properties +// // including the stream and schema name +// READ_FILTER_SCOPE_RECORD = 4; +//} +// +//// The filter to apply when reading records from the database +//// The combination of stream scope and literal expression indicates a direct stream name match, +//// while a regex expression indicates a pattern match across multiple streams. +//message ReadFilter { +// // The scope of the filter. +// ReadFilterScope scope = 1; +// // The expression can be a regular expression or a literal value. +// // If it starts with "~" it will be considered a regex. +// string expression = 2; +// +// // // The optional name of the record property to filter on. +// // optional string property_name = 3; +// +// // The optional property names to filter on. +// repeated string property_names = 4; +//} +// +//// Record retrieved from the database. +//message Record { +// // The unique identifier of the record in the database. +// string record_id = 1; +// // The position of the record in the database. +// int64 position = 5; +// // The actual data payload of the record, stored as bytes. +// bytes data = 2; +// // Additional information about the record. +// map properties = 3; +// // When the record was created. +// google.protobuf.Timestamp timestamp = 4; +// // The stream to which the record belongs. +// optional string stream = 6; +// // The revision of the stream created when the record was appended. +// optional int64 stream_revision = 7; +//} +// +//// The direction in which to read records from the database (forwards or backwards). +//enum ReadDirection { +// READ_DIRECTION_FORWARDS = 0; +// READ_DIRECTION_BACKWARDS = 1; +//} +// +//// The position from which to start reading records. +//// This can be either the earliest or latest position in the stream. +//enum ReadPositionConstants { +// READ_POSITION_CONSTANTS_UNSPECIFIED = 0; +// READ_POSITION_CONSTANTS_EARLIEST = 1; +// READ_POSITION_CONSTANTS_LATEST = 2; +//} +// +//// Represents the successful outcome of a read operation. +//message ReadSuccess { +// repeated Record records = 1; +//} +// +//// Represents the detailed error information when a read operation fails. +//message ReadFailure { +// // The error details +// oneof error { +// // Failed because the client lacks sufficient permissions. +// ErrorDetails.AccessDenied access_denied = 1; +// // Failed because the target stream has been deleted. +// ErrorDetails.StreamDeleted stream_deleted = 2; +// // Failed because the expected stream revision did not match the actual revision. +// ErrorDetails.StreamNotFound stream_not_found = 3; +// } +//} +// +//message ReadRequest { +// // The filter to apply when reading records. +// optional ReadFilter filter = 1; +// // The starting position of the log from which to read records. +// optional int64 start_position = 2; +// // Limit how many records can be returned. +// // This will get capped at the default limit, +// // which is up to 1000 records. +// optional int64 limit = 3; +// // The direction in which to read the stream (forwards or backwards). +// ReadDirection direction = 4; +// // Heartbeats can be enabled to monitor end-to-end session health. +// HeartbeatOptions heartbeats = 5; +// // The number of records to read in a single batch. +// int32 batch_size = 6; +//} +// +////message SubscriptionConfirmed { +//// // The subscription ID that was confirmed. +//// string subscription_id = 1; +//// // The position of the last record read by the server. +//// optional int64 position = 2; +//// // When the subscription was confirmed. +//// google.protobuf.Timestamp timestamp = 3; +////} +// +//// Read session response. +//message ReadResponse { +// oneof result { +// // Success represents the successful outcome of an read operation. +// ReadSuccess success = 1; +// // Failure represents the details of a failed read operation. +// ReadFailure failure = 2; +// // Heartbeat represents the health check of the read operation when +// // the server has not found any records matching the filter for the specified +// // period of time or records threshold. +// // A heartbeat will be sent when the initial switch to real-time tailing happens. +// Heartbeat heartbeat = 3; +// } +//} +// +//// A health check will be sent when the server has not found any records +//// matching the filter for the specified period of time or records threshold. A +//// heartbeat will be sent when the initial switch to real-time tailing happens. +//message HeartbeatOptions { +// bool enable = 1; +// optional google.protobuf.Duration period = 2; // 30 seconds +// optional int32 records_threshold = 3; // 500 +//} +// +//enum HeartbeatType { +// HEARTBEAT_TYPE_UNSPECIFIED = 0; +// HEARTBEAT_TYPE_CHECKPOINT = 1; +// HEARTBEAT_TYPE_CAUGHT_UP = 2; +// HEARTBEAT_TYPE_FELL_BEHIND = 3; +//} +// +//message Heartbeat { +// // This indicates whether the subscription is caught up, fell behind, or +// // the filter has not been satisfied after a period of time or records threshold. +// HeartbeatType type = 1; +// // Checkpoint for resuming reads. +// // It will always be populated unless the database is empty. +// int64 position = 2; +// // When the heartbeat was sent. +// google.protobuf.Timestamp timestamp = 3; +//} + +////=================================================================== +//// Read Operations +////=================================================================== +// +//enum ConsumeFilterScope { +// CONSUME_FILTER_SCOPE_UNSPECIFIED = 0; +// // The filter will be applied to the stream name +// CONSUME_FILTER_SCOPE_STREAM = 1; +// // The filter will be applied to the record schema name +// CONSUME_FILTER_SCOPE_RECORD = 2; +// // The filter will be applied to the properties of record +// CONSUME_FILTER_SCOPE_PROPERTIES = 3; +// // The filter will be applied to the record data +// CONSUME_FILTER_SCOPE_DATA = 4; +//} +// +//// The filter to apply when reading records from the database +//// It applies to a stream or a record +//message ConsumeFilter { +// // The scope of the filter. +// ConsumeFilterScope scope = 1; +// // The expression can be a regular expression, a jsonpath expression, or a literal value. +// // if it starts with "~" it will be considered a regex and if it starts with "$" it will be considered a jsonpath filter, else its a literal. +// string expression = 2; +// // The name of the record property to filter on. +// optional string property_name = 3; +//} +// +//// Record retrieved from the database. +//message Record { +// // The unique identifier of the record in the database. +// string record_id = 1; +// // The position of the record in the database. +// int64 position = 5; +// // The actual data payload of the record, stored as bytes. +// bytes data = 2; +// // Additional information about the record. +// map properties = 3; +// // When the record was created. +// google.protobuf.Timestamp timestamp = 4; +// // The stream to which the record belongs. +// optional string stream = 6; +// // The revision of the stream created when the record was appended. +// optional int64 stream_revision = 7; +//} +// +////// A batch of records. +////message RecordBatch { +//// repeated Record records = 1; +////} +// +//// The direction in which to read records from the database (forwards or backwards). +//enum ReadDirection { +// READ_DIRECTION_FORWARDS = 0; +// READ_DIRECTION_BACKWARDS = 1; +//} +// +//// The position from which to start reading records. +//// This can be either the earliest or latest position in the stream. +//enum ReadPositionConstants { +// READ_POSITION_CONSTANTS_UNSPECIFIED = 0; +// READ_POSITION_CONSTANTS_EARLIEST = 1; +// READ_POSITION_CONSTANTS_LATEST = 2; +//} +// +//message ReadStreamRequest { +// // The filter to apply when reading records. +// optional ConsumeFilter filter = 1; +// // The starting position of the log from which to read records. +// optional int64 start_position = 2; +// // Limit how many records can be returned. +// // This will get capped at the default limit, +// // which is up to 1000 records. +// optional int64 limit = 3; +// // The direction in which to read the stream (forwards or backwards). +// ReadDirection direction = 4; +//} +// +//message ReadStreamSuccess { +// repeated Record records = 1; +//} +// +//// Represents the detailed error information when a read operation fails. +//message ReadStreamFailure { +// // The error details +// oneof error { +// // Failed because the client lacks sufficient permissions. +// ErrorDetails.AccessDenied access_denied = 3; +// // Failed because the target stream has been deleted. +// ErrorDetails.StreamDeleted stream_deleted = 4; +// } +//} +//message ReadStreamResponse { +// // The result of the read operation. +// oneof result { +// // Success represents the successful outcome of an read operation. +// ReadStreamSuccess success = 1; +// // Failure represents the details of a failed read operation. +// ReadStreamFailure failure = 2; +// // Heartbeat represents the health check of the read operation when +// // the server has not found any records matching the filter for the specified +// // period of time or records threshold. +// // A heartbeat will be sent when the initial switch to real-time tailing happens. +// Heartbeat heartbeat = 3; +// } +//} +// +//message ReadSessionRequest { +// // The filter to apply when reading records. +// optional ConsumeFilter filter = 1; +// // The starting position of the log from which to read records. +// optional int64 start_position = 2; +// // Limit how many records can be returned. +// // This will get capped at the default limit, +// // which is up to 1000 records. +// optional int64 limit = 3; +// // The direction in which to read the stream (forwards or backwards). +// ReadDirection direction = 4; +// // Heartbeats can be enabled to monitor end-to-end session health. +// HeartbeatOptions heartbeats = 5; +//} +// +//// Read session response. +//message ReadSessionResponse { +// oneof result { +// // Success represents the successful outcome of an read operation. +// ReadStreamSuccess success = 1; +// // Failure represents the details of a failed read operation. +// ReadStreamFailure failure = 2; +// // Heartbeat represents the health check of the read operation when +// // the server has not found any records matching the filter for the specified +// // period of time or records threshold. +// // A heartbeat will be sent when the initial switch to real-time tailing happens. +// Heartbeat heartbeat = 3; +// } +//} +// +//// A health check will be sent when the server has not found any records +//// matching the filter for the specified period of time or records threshold. A +//// heartbeat will be sent when the initial switch to real-time tailing happens. +//message HeartbeatOptions { +// bool enable = 1; +// //optional google.protobuf.Duration period = 2; +// optional int32 records_threshold = 3; // 1000 +//} +// +//enum HeartbeatType { +// HEARTBEAT_TYPE_UNSPECIFIED = 0; +// HEARTBEAT_TYPE_CHECKPOINT = 1; +// HEARTBEAT_TYPE_CAUGHT_UP = 2; +//} +// +//message Heartbeat { +// // This indicates whether the subscription is caught up, fell behind, or +// // the filter has not been satisfied after a period of time or records threshold. +// HeartbeatType type = 1; +// // Checkpoint for resuming reads. +// // It will always be populated unless the database is empty. +// int64 position = 2; +// // When the heartbeat was sent. +// google.protobuf.Timestamp timestamp = 3; +//} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_manage.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_manage.proto new file mode 100644 index 000000000..639439794 --- /dev/null +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_manage.proto @@ -0,0 +1,123 @@ +syntax = "proto3"; + +package kurrentdb.protocol.v2; + +option csharp_namespace = "KurrentDB.Protocol.Streams.V2"; +option java_package = "io.kurrentdb.protocol.streams.v2"; +option java_multiple_files = true; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/descriptor.proto"; + +import "streams/error_details.proto"; +import "core.proto"; + +service StreamsManagementService { + rpc DeleteStream(DeleteStreamRequest) returns (DeleteStreamResponse); + + rpc GetStreamInfo(DeleteStreamRequest) returns (DeleteStreamResponse); +} + +message DeleteStreamRequest { + // The name of the stream. + string stream = 1; + // The expected revision of the stream. If the stream's current revision does + // not match, the operation will fail. + // The expected revision can also be one of the special values + // from ExpectedRevisionConstants. + // Missing value means no expectation, the same as EXPECTED_REVISION_CONSTANTS_ANY + optional sint64 expected_revision = 3; + + // If true, the stream will be tombstoned instead of deleted. + // A tombstoned stream cannot be used anymore. + bool tombstone = 4; +} + +message DeleteStreamSuccess { + // The position of the last appended record in the stream. + int64 position = 2; + // The expected revision of the stream after the append operation. + int64 stream_revision = 3; +} + +message DeleteStreamFailure { + // The error details + oneof error { + // Failed because the actual stream revision didn't match the expected revision. + StreamsErrorDetails.StreamRevisionConflict stream_revision_conflict = 2; + // Failed because the client lacks sufficient permissions. + CoreErrorDetails.AccessDenied access_denied = 3; + // Failed because the target stream has been deleted. + StreamsErrorDetails.StreamDeleted stream_deleted = 4; + // Failed because the stream has been tombstoned. + StreamsErrorDetails.StreamTombstoned stream_tombstoned = 5; + // Failed because the stream was not found. + StreamsErrorDetails.StreamNotFound stream_not_found = 6; + } +} + +message DeleteStreamResponse { + // The result of the operation. + oneof result { + // Success represents the successful outcome of a delete operation. + DeleteStreamSuccess success = 1; + // Failure represents the details of a failed delete operation. + DeleteStreamFailure failure = 2; + } +} + +// Represents the access control list (ACL) for a stream. +// The ACL defines who can read, write, delete, or manage the stream. +message StreamAcl { + // Roles and users permitted to read the stream + repeated string read_roles = 1; + // Roles and users permitted to write to the stream + repeated string write_roles = 2; + // Roles and users permitted to delete the stream + repeated string delete_roles = 3; + // Roles and users permitted to read stream metadata + repeated string meta_read_roles = 4; + // Roles and users permitted to write stream metadata + repeated string meta_write_roles = 5; +} + +message StreamMetadata { + // Maximum age of events allowed in the stream. + optional google.protobuf.Duration max_age = 1; + // Stream revision from which previous events can be scavenged. + optional int64 TruncateBefore = 2; + // Amount of time for which the stream head is cacheable. + optional google.protobuf.Duration cache_control = 4; + // Maximum number of events allowed in the stream. + optional int32 MaxCount = 5; + // User-provided metadata. + optional string custom_metadata = 6; + // Access control list for the stream. + StreamAcl acl = 7; +} + +// Represents the state of a stream. +// The state is used to determine how the stream should be handled +// during operations such as deletion or retrieval. +enum StreamState { + NotFound = 0; + Active = 1; + Deleted = 2; + Tombstoned = 3; +} + +// Represents the information about a stream. +message StreamInfo { + // The last stream revision of the stream. + int64 last_revision = 1; + // The last position in the stream. + int64 last_position = 2; + // The state of the stream. + StreamState State = 5; + // The metadata associated with the stream. + StreamMetadata metadata = 4; + // The revision of the stream metadata. + // This is the revision of the metadata, not the stream itself. + int64 metadata_revision = 3; +} diff --git a/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_read.proto b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_read.proto new file mode 100644 index 000000000..77b567739 --- /dev/null +++ b/src/KurrentDB.Client/Core/proto/kurrentdb/protocol/v2/streams/streams_read.proto @@ -0,0 +1,200 @@ +syntax = "proto3"; + +package kurrentdb.protocol.v2; + +option csharp_namespace = "KurrentDB.Protocol.Streams.V2"; +option java_package = "io.kurrentdb.protocol.streams.v2"; +option java_multiple_files = true; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/descriptor.proto"; + +import "streams/error_details.proto"; +import "core.proto"; + +service StreamsReadService { + // Retrieve batches of records continuously. + rpc ReadSession(ReadRequest) returns (stream ReadResponse); + + // Retrieve batches of records continuously. + rpc Read(ReadRequest) returns (stream ReadResponse); +} + +//=================================================================== +// Read Operations +//=================================================================== + +// The scope of the read filter determines where the filter will be applied. +enum ReadFilterScope { + READ_FILTER_SCOPE_UNSPECIFIED = 0; + // The filter will be applied to the record stream name + READ_FILTER_SCOPE_STREAM = 1; + // The filter will be applied to the record schema name + READ_FILTER_SCOPE_SCHEMA_NAME = 2; + // The filter will be applied to the properties of the record + READ_FILTER_SCOPE_PROPERTIES = 3; + // The filter will be applied to all the record properties + // including the stream and schema name + READ_FILTER_SCOPE_RECORD = 4; + // The filter will be applied to the index name + READ_FILTER_SCOPE_INDEX_NAME = 5; +} + +// The filter to apply when reading records from the database +// The combination of stream scope and literal expression indicates a direct stream name match, +// while a regex expression indicates a pattern match across multiple streams. +message ReadFilter { + // The scope of the filter. + ReadFilterScope scope = 1; + // The expression can be a regular expression or a literal value. + // If it starts with "~" it will be considered a regex. + string expression = 2; +} + +// Record retrieved from the database. +message Record { + // The unique identifier of the record in the database. + string record_id = 1; + // The position of the record in the database. + int64 position = 5; + // The actual data payload of the record, stored as bytes. + bytes data = 2; + // Additional information about the record. + map properties = 3; + // When the record was created. + google.protobuf.Timestamp timestamp = 4; + // The stream to which the record belongs. + optional string stream = 6; + // The revision of the stream created when the record was appended. + optional int64 stream_revision = 7; +} + +// The direction in which to read records from the database (forwards or backwards). +enum ReadDirection { + READ_DIRECTION_FORWARDS = 0; + READ_DIRECTION_BACKWARDS = 1; +} + +// Initial position at which the cursor will be set when subscribing. +enum ReadInitialPosition { + READ_INITIAL_POSITION_LATEST = 0; + READ_INITIAL_POSITION_EARLIEST = 1; +} + +// Limit how many records can be retrieved. +message ReadLimit { + // The maximum number of records to return. + // If not specified, an active read session will be created + // and the client will receive records as they are appended to the stream. + optional uint64 count = 1; +} + +// Represents the successful outcome of a read operation. +message ReadSuccess { + repeated Record records = 1; +} + +// Represents the detailed error information when a read operation fails. +message ReadFailure { + // The error details + oneof error { + // Failed because the client lacks sufficient permissions. + CoreErrorDetails.AccessDenied access_denied = 1; + // Failed because the target stream has been deleted. + StreamsErrorDetails.StreamDeleted stream_deleted = 2; + // Failed because the stream has been tombstoned. + StreamsErrorDetails.StreamTombstoned stream_tombstoned = 4; + // Failed because the expected stream revision did not match the actual revision. + StreamsErrorDetails.StreamNotFound stream_not_found = 3; + } +} + +// Read request message that contains the filter, starting position, limit, +// direction, and heartbeat options for reading records from the database. +// It allows the client to specify how to read records, including the filter to apply, +// the starting position, the maximum number of records to return, the direction of reading, +// and whether to enable heartbeats for monitoring session health. +// The batch size can also be specified to control how many records are read in a single batch. +message ReadRequest { + // The filter to apply when reading records. + optional ReadFilter filter = 1; + // The starting position of the log from which to read records. + optional int64 start_position = 2; + + // Position at which the cursor will be set when subscribing. + // Used when no start position is specified. + // If not specified, the default is READ_INITIAL_POSITION_LATEST. + ReadInitialPosition initial_position = 7; + + // Limit how many records can be returned. + // This will get capped at the default limit, + // which is up to 1000 records. + optional int64 limit = 3; + // The direction in which to read the stream (forwards or backwards). + ReadDirection direction = 4; + // Heartbeats can be enabled to monitor end-to-end session health. + HeartbeatOptions heartbeats = 5; + // The number of records to read in a single batch. + int32 batch_size = 6; +} + +// Read response message that can contain either a successful read result, +// a failure result, or a heartbeat message. +// This allows the client to handle different outcomes of the read operation. +message ReadResponse { + oneof result { + // Success represents the successful outcome of an read operation. + ReadSuccess success = 1; + // Failure represents the details of a failed read operation. + ReadFailure failure = 2; + // Heartbeat represents the health check of the read operation when + // the server has not found any records matching the filter for the specified + // period of time or records threshold. + // A heartbeat will be sent when the initial switch to real-time tailing happens. + Heartbeat heartbeat = 3; + } +} + +// A health check will be sent when the server has not found any records +// matching the filter for the specified period of time or records threshold. A +// heartbeat will be sent when the initial switch to real-time tailing happens. +message HeartbeatOptions { + // Enable heartbeats for monitoring end-to-end session health. + // If enabled, heartbeats will be sent periodically or when the records threshold is reached. + // If disabled, no heartbeats will be sent. + bool enable = 1; + // The period after which a heartbeat will be sent if no records are found. + // If not specified, the default period is 30 seconds. + // This is the maximum time to wait before sending a heartbeat. + // If the period is set to 0, heartbeats will not be sent based on time. + // If the period is set to a non-zero value, heartbeats will be sent + optional google.protobuf.Duration period = 2; // 30 seconds + optional int32 records_threshold = 3; // 500 +} + +// The type of heartbeat sent by the server to indicate the status of the read operation. +// It can indicate whether the subscription is caught up, fell behind, or +// the filter has not been satisfied after a period of time or records threshold. +enum HeartbeatType { + HEARTBEAT_TYPE_UNSPECIFIED = 0; + HEARTBEAT_TYPE_CHECKPOINT = 1; + HEARTBEAT_TYPE_CAUGHT_UP = 2; + HEARTBEAT_TYPE_FELL_BEHIND = 3; +} + +// Heartbeat message sent by the server to indicate the status of the read operation. +// It contains the type of heartbeat, the position for resuming reads, +// and the timestamp when the heartbeat was sent. +message Heartbeat { + // This indicates whether the subscription is caught up, fell behind, or + // the filter has not been satisfied after a period of time or records threshold. + HeartbeatType type = 1; + // Checkpoint for resuming reads. + // It will always be populated unless the database is empty. + int64 position = 2; + // When the heartbeat was sent. + google.protobuf.Timestamp timestamp = 3; +} + +//=================================================================== diff --git a/src/KurrentDB.Client/Core/protos/streams.proto b/src/KurrentDB.Client/Core/protos/streams.proto deleted file mode 100644 index 0eb05295c..000000000 --- a/src/KurrentDB.Client/Core/protos/streams.proto +++ /dev/null @@ -1,316 +0,0 @@ -syntax = "proto3"; -package event_store.client.streams; -option java_package = "io.kurrent.dbclient.proto.streams"; - -import "shared.proto"; -import "status.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/empty.proto"; -import "google/protobuf/timestamp.proto"; - -service Streams { - rpc Read (ReadReq) returns (stream ReadResp); - rpc Append (stream AppendReq) returns (AppendResp); - rpc Delete (DeleteReq) returns (DeleteResp); - rpc Tombstone (TombstoneReq) returns (TombstoneResp); - rpc BatchAppend (stream BatchAppendReq) returns (stream BatchAppendResp); -} - -message ReadReq { - Options options = 1; - - message Options { - oneof stream_option { - StreamOptions stream = 1; - AllOptions all = 2; - } - ReadDirection read_direction = 3; - bool resolve_links = 4; - oneof count_option { - uint64 count = 5; - SubscriptionOptions subscription = 6; - } - oneof filter_option { - FilterOptions filter = 7; - event_store.client.Empty no_filter = 8; - } - UUIDOption uuid_option = 9; - ControlOption control_option = 10; - - enum ReadDirection { - Forwards = 0; - Backwards = 1; - } - message StreamOptions { - event_store.client.StreamIdentifier stream_identifier = 1; - oneof revision_option { - uint64 revision = 2; - event_store.client.Empty start = 3; - event_store.client.Empty end = 4; - } - } - message AllOptions { - oneof all_option { - Position position = 1; - event_store.client.Empty start = 2; - event_store.client.Empty end = 3; - } - } - message SubscriptionOptions { - } - message Position { - uint64 commit_position = 1; - uint64 prepare_position = 2; - } - message FilterOptions { - oneof filter { - Expression stream_identifier = 1; - Expression event_type = 2; - } - oneof window { - uint32 max = 3; - event_store.client.Empty count = 4; - } - uint32 checkpointIntervalMultiplier = 5; - - message Expression { - string regex = 1; - repeated string prefix = 2; - } - } - message UUIDOption { - oneof content { - event_store.client.Empty structured = 1; - event_store.client.Empty string = 2; - } - } - message ControlOption { - uint32 compatibility = 1; - } - } -} - -message ReadResp { - oneof content { - ReadEvent event = 1; - SubscriptionConfirmation confirmation = 2; - Checkpoint checkpoint = 3; - StreamNotFound stream_not_found = 4; - uint64 first_stream_position = 5; - uint64 last_stream_position = 6; - AllStreamPosition last_all_stream_position = 7; - CaughtUp caught_up = 8; - FellBehind fell_behind = 9; - } - - message CaughtUp {} - - message FellBehind {} - - message ReadEvent { - RecordedEvent event = 1; - RecordedEvent link = 2; - oneof position { - uint64 commit_position = 3; - event_store.client.Empty no_position = 4; - } - - message RecordedEvent { - event_store.client.UUID id = 1; - event_store.client.StreamIdentifier stream_identifier = 2; - uint64 stream_revision = 3; - uint64 prepare_position = 4; - uint64 commit_position = 5; - map metadata = 6; - bytes custom_metadata = 7; - bytes data = 8; - } - } - message SubscriptionConfirmation { - string subscription_id = 1; - } - message Checkpoint { - uint64 commit_position = 1; - uint64 prepare_position = 2; - } - message StreamNotFound { - event_store.client.StreamIdentifier stream_identifier = 1; - } -} - -message AppendReq { - oneof content { - Options options = 1; - ProposedMessage proposed_message = 2; - } - - message Options { - event_store.client.StreamIdentifier stream_identifier = 1; - oneof expected_stream_revision { - uint64 revision = 2; - event_store.client.Empty no_stream = 3; - event_store.client.Empty any = 4; - event_store.client.Empty stream_exists = 5; - } - } - message ProposedMessage { - event_store.client.UUID id = 1; - map metadata = 2; - bytes custom_metadata = 3; - bytes data = 4; - } -} - -message AppendResp { - oneof result { - Success success = 1; - WrongExpectedVersion wrong_expected_version = 2; - } - - message Position { - uint64 commit_position = 1; - uint64 prepare_position = 2; - } - - message Success { - oneof current_revision_option { - uint64 current_revision = 1; - event_store.client.Empty no_stream = 2; - } - oneof position_option { - Position position = 3; - event_store.client.Empty no_position = 4; - } - } - - message WrongExpectedVersion { - oneof current_revision_option_20_6_0 { - uint64 current_revision_20_6_0 = 1; - event_store.client.Empty no_stream_20_6_0 = 2; - } - oneof expected_revision_option_20_6_0 { - uint64 expected_revision_20_6_0 = 3; - event_store.client.Empty any_20_6_0 = 4; - event_store.client.Empty stream_exists_20_6_0 = 5; - } - oneof current_revision_option { - uint64 current_revision = 6; - event_store.client.Empty current_no_stream = 7; - } - oneof expected_revision_option { - uint64 expected_revision = 8; - event_store.client.Empty expected_any = 9; - event_store.client.Empty expected_stream_exists = 10; - event_store.client.Empty expected_no_stream = 11; - } - - } -} - -message BatchAppendReq { - event_store.client.UUID correlation_id = 1; - Options options = 2; - repeated ProposedMessage proposed_messages = 3; - bool is_final = 4; - - message Options { - event_store.client.StreamIdentifier stream_identifier = 1; - oneof expected_stream_position { - uint64 stream_position = 2; - google.protobuf.Empty no_stream = 3; - google.protobuf.Empty any = 4; - google.protobuf.Empty stream_exists = 5; - } - oneof deadline_option { - google.protobuf.Timestamp deadline_21_10_0 = 6; - google.protobuf.Duration deadline = 7; - } - } - - message ProposedMessage { - event_store.client.UUID id = 1; - map metadata = 2; - bytes custom_metadata = 3; - bytes data = 4; - } -} - -message BatchAppendResp { - event_store.client.UUID correlation_id = 1; - oneof result { - google.rpc.Status error = 2; - Success success = 3; - } - - event_store.client.StreamIdentifier stream_identifier = 4; - - oneof expected_stream_position { - uint64 stream_position = 5; - google.protobuf.Empty no_stream = 6; - google.protobuf.Empty any = 7; - google.protobuf.Empty stream_exists = 8; - } - - message Success { - oneof current_revision_option { - uint64 current_revision = 1; - google.protobuf.Empty no_stream = 2; - } - oneof position_option { - event_store.client.AllStreamPosition position = 3; - google.protobuf.Empty no_position = 4; - } - } -} - -message DeleteReq { - Options options = 1; - - message Options { - event_store.client.StreamIdentifier stream_identifier = 1; - oneof expected_stream_revision { - uint64 revision = 2; - event_store.client.Empty no_stream = 3; - event_store.client.Empty any = 4; - event_store.client.Empty stream_exists = 5; - } - } -} - -message DeleteResp { - oneof position_option { - Position position = 1; - event_store.client.Empty no_position = 2; - } - - message Position { - uint64 commit_position = 1; - uint64 prepare_position = 2; - } -} - -message TombstoneReq { - Options options = 1; - - message Options { - event_store.client.StreamIdentifier stream_identifier = 1; - oneof expected_stream_revision { - uint64 revision = 2; - event_store.client.Empty no_stream = 3; - event_store.client.Empty any = 4; - event_store.client.Empty stream_exists = 5; - } - } -} - -message TombstoneResp { - oneof position_option { - Position position = 1; - event_store.client.Empty no_position = 2; - } - - message Position { - uint64 commit_position = 1; - uint64 prepare_position = 2; - } -} diff --git a/src/KurrentDB.Client/KurrentDB.Client.csproj b/src/KurrentDB.Client/KurrentDB.Client.csproj index 95d465f45..8821fe674 100644 --- a/src/KurrentDB.Client/KurrentDB.Client.csproj +++ b/src/KurrentDB.Client/KurrentDB.Client.csproj @@ -21,77 +21,25 @@ - - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - - - MSBuild:Compile - + + + - - - - - - - - + + + diff --git a/src/KurrentDB.Client/Operations/KurrentDBOperationsClient.Admin.cs b/src/KurrentDB.Client/Operations/KurrentDBOperationsClient.Admin.cs index 2e3738c27..3d9146dca 100644 --- a/src/KurrentDB.Client/Operations/KurrentDBOperationsClient.Admin.cs +++ b/src/KurrentDB.Client/Operations/KurrentDBOperationsClient.Admin.cs @@ -1,5 +1,6 @@ using EventStore.Client; -using EventStore.Client.Operations; +using KurrentDB.Protocol.Operations.V1; +using static KurrentDB.Protocol.Operations.V1.Operations; namespace KurrentDB.Client { public partial class KurrentDBOperationsClient { @@ -17,7 +18,7 @@ public async Task ShutdownAsync( UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Operations.OperationsClient( + using var call = new OperationsClient( channelInfo.CallInvoker).ShutdownAsync(EmptyResult, KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); @@ -35,7 +36,7 @@ public async Task MergeIndexesAsync( UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Operations.OperationsClient( + using var call = new OperationsClient( channelInfo.CallInvoker).MergeIndexesAsync(EmptyResult, KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); @@ -53,7 +54,7 @@ public async Task ResignNodeAsync( UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Operations.OperationsClient( + using var call = new OperationsClient( channelInfo.CallInvoker).ResignNodeAsync(EmptyResult, KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); @@ -72,7 +73,7 @@ public async Task SetNodePriorityAsync(int nodePriority, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Operations.OperationsClient( + using var call = new OperationsClient( channelInfo.CallInvoker).SetNodePriorityAsync( new SetNodePriorityReq {Priority = nodePriority}, KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); @@ -91,7 +92,7 @@ public async Task RestartPersistentSubscriptions( UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Operations.OperationsClient( + using var call = new OperationsClient( channelInfo.CallInvoker).RestartPersistentSubscriptionsAsync( EmptyResult, KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); diff --git a/src/KurrentDB.Client/Operations/KurrentDBOperationsClient.Scavenge.cs b/src/KurrentDB.Client/Operations/KurrentDBOperationsClient.Scavenge.cs index fc1cbaafd..a0691418e 100644 --- a/src/KurrentDB.Client/Operations/KurrentDBOperationsClient.Scavenge.cs +++ b/src/KurrentDB.Client/Operations/KurrentDBOperationsClient.Scavenge.cs @@ -1,4 +1,5 @@ -using EventStore.Client.Operations; +using KurrentDB.Protocol.Operations.V1; +using static KurrentDB.Protocol.Operations.V1.Operations; namespace KurrentDB.Client { public partial class KurrentDBOperationsClient { @@ -27,7 +28,7 @@ public async Task StartScavengeAsync( } var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - using var call = new Operations.OperationsClient( + using var call = new OperationsClient( channelInfo.CallInvoker).StartScavengeAsync( new StartScavengeReq { Options = new StartScavengeReq.Types.Options { @@ -60,7 +61,7 @@ public async Task StopScavengeAsync( UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - var result = await new Operations.OperationsClient( + var result = await new OperationsClient( channelInfo.CallInvoker).StopScavengeAsync(new StopScavengeReq { Options = new StopScavengeReq.Types.Options { ScavengeId = scavengeId diff --git a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Create.cs b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Create.cs index 4b82c53f5..7f4e76e1f 100644 --- a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Create.cs +++ b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Create.cs @@ -1,5 +1,6 @@ using EventStore.Client; -using EventStore.Client.PersistentSubscriptions; +using KurrentDB.Protocol.PersistentSubscriptions.V1; +using static KurrentDB.Protocol.PersistentSubscriptions.V1.PersistentSubscriptions; namespace KurrentDB.Client { partial class KurrentDBPersistentSubscriptionsClient { @@ -9,7 +10,7 @@ partial class KurrentDBPersistentSubscriptionsClient { [SystemConsumerStrategies.RoundRobin] = CreateReq.Types.ConsumerStrategy.RoundRobin, [SystemConsumerStrategies.Pinned] = CreateReq.Types.ConsumerStrategy.Pinned, }; - + private static CreateReq.Types.StreamOptions StreamOptionsForCreateProto(string streamName, StreamPosition position) { if (position == StreamPosition.Start) { return new CreateReq.Types.StreamOptions { @@ -202,7 +203,7 @@ private async Task CreateInternalAsync(string streamName, string groupName, IEve throw new InvalidOperationException("The server does not support persistent subscriptions to $all."); } - using var call = new PersistentSubscriptions.PersistentSubscriptionsClient( + using var call = new PersistentSubscriptionsClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { Stream = streamName != SystemStreams.AllStream diff --git a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Delete.cs b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Delete.cs index 252cf27a5..5a94ebdae 100644 --- a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Delete.cs +++ b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Delete.cs @@ -1,5 +1,6 @@ using EventStore.Client; -using EventStore.Client.PersistentSubscriptions; +using KurrentDB.Protocol.PersistentSubscriptions.V1; +using static KurrentDB.Protocol.PersistentSubscriptions.V1.PersistentSubscriptions; namespace KurrentDB.Client { partial class KurrentDBPersistentSubscriptionsClient { @@ -34,7 +35,7 @@ public async Task DeleteToStreamAsync(string streamName, string groupName, TimeS } using var call = - new PersistentSubscriptions.PersistentSubscriptionsClient( + new PersistentSubscriptionsClient( channelInfo.CallInvoker) .DeleteAsync(new DeleteReq {Options = deleteOptions}, KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, diff --git a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Info.cs b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Info.cs index 5c501c1e4..d5c5504b2 100644 --- a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Info.cs +++ b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Info.cs @@ -1,5 +1,6 @@ using EventStore.Client; -using EventStore.Client.PersistentSubscriptions; +using KurrentDB.Protocol.PersistentSubscriptions.V1; +using static KurrentDB.Protocol.PersistentSubscriptions.V1.PersistentSubscriptions; using Grpc.Core; #nullable enable @@ -53,7 +54,7 @@ public async Task GetInfoToStreamAsync(string stream private async Task GetInfoGrpcAsync(GetInfoReq req, TimeSpan? deadline, UserCredentials? userCredentials, CallInvoker callInvoker, CancellationToken cancellationToken) { - var result = await new PersistentSubscriptions.PersistentSubscriptionsClient(callInvoker) + var result = await new PersistentSubscriptionsClient(callInvoker) .GetInfoAsync(req, KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) .ConfigureAwait(false); diff --git a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.List.cs b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.List.cs index 4d29c6579..7736c05e9 100644 --- a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.List.cs +++ b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.List.cs @@ -1,6 +1,7 @@ using EventStore.Client; using Grpc.Core; -using EventStore.Client.PersistentSubscriptions; +using KurrentDB.Protocol.PersistentSubscriptions.V1; +using static KurrentDB.Protocol.PersistentSubscriptions.V1.PersistentSubscriptions; #nullable enable namespace KurrentDB.Client { @@ -85,7 +86,7 @@ public async Task> ListAllAsync(TimeSpan private async Task> ListGrpcAsync(ListReq req, TimeSpan? deadline, UserCredentials? userCredentials, CallInvoker callInvoker, CancellationToken cancellationToken) { - using var call = new PersistentSubscriptions.PersistentSubscriptionsClient(callInvoker) + using var call = new PersistentSubscriptionsClient(callInvoker) .ListAsync(req, KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); ListResp? response = await call.ResponseAsync.ConfigureAwait(false); diff --git a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Read.cs b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Read.cs index d90f9d5c6..78bfaf168 100644 --- a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Read.cs +++ b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Read.cs @@ -1,9 +1,10 @@ using System.Threading.Channels; using EventStore.Client; -using EventStore.Client.PersistentSubscriptions; using KurrentDB.Client.Diagnostics; using Grpc.Core; -using static EventStore.Client.PersistentSubscriptions.ReadResp.ContentOneofCase; +using KurrentDB.Protocol.PersistentSubscriptions.V1; +using static KurrentDB.Protocol.PersistentSubscriptions.V1.PersistentSubscriptions; +using static KurrentDB.Protocol.PersistentSubscriptions.V1.ReadResp.ContentOneofCase; namespace KurrentDB.Client { partial class KurrentDBPersistentSubscriptionsClient { @@ -247,7 +248,7 @@ CancellationToken cancellationToken async Task PumpMessages() { try { var channelInfo = await selectChannelInfo(_cts.Token).ConfigureAwait(false); - var client = new PersistentSubscriptions.PersistentSubscriptionsClient(channelInfo.CallInvoker); + var client = new PersistentSubscriptionsClient(channelInfo.CallInvoker); _call = client.Read(_callOptions); diff --git a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.ReplayParked.cs b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.ReplayParked.cs index 3dcf3b77e..7627ad2d1 100644 --- a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.ReplayParked.cs +++ b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.ReplayParked.cs @@ -1,6 +1,7 @@ using Grpc.Core; using EventStore.Client; -using EventStore.Client.PersistentSubscriptions; +using KurrentDB.Protocol.PersistentSubscriptions.V1; +using static KurrentDB.Protocol.PersistentSubscriptions.V1.PersistentSubscriptions; using NotSupportedException = System.NotSupportedException; #nullable enable @@ -72,7 +73,7 @@ private async Task ReplayParkedGrpcAsync(ReplayParkedReq req, long? numberOfEven req.Options.NoLimit = new Empty(); } - await new PersistentSubscriptions.PersistentSubscriptionsClient(callInvoker) + await new PersistentSubscriptionsClient(callInvoker) .ReplayParkedAsync(req, KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) .ConfigureAwait(false); } diff --git a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.RestartSubsystem.cs b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.RestartSubsystem.cs index 4139aa5bf..67468b8a2 100644 --- a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.RestartSubsystem.cs +++ b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.RestartSubsystem.cs @@ -1,6 +1,6 @@ #nullable enable using EventStore.Client; -using EventStore.Client.PersistentSubscriptions; +using static KurrentDB.Protocol.PersistentSubscriptions.V1.PersistentSubscriptions; namespace KurrentDB.Client { partial class KurrentDBPersistentSubscriptionsClient { @@ -12,7 +12,7 @@ public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentia var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsRestartSubsystem) { - await new PersistentSubscriptions.PersistentSubscriptionsClient(channelInfo.CallInvoker) + await new PersistentSubscriptionsClient(channelInfo.CallInvoker) .RestartSubsystemAsync(new Empty(), KurrentDBCallOptions .CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) .ConfigureAwait(false); diff --git a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Update.cs b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Update.cs index 2905a8b9a..37a42686f 100644 --- a/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Update.cs +++ b/src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.Update.cs @@ -1,5 +1,6 @@ using EventStore.Client; -using EventStore.Client.PersistentSubscriptions; +using KurrentDB.Protocol.PersistentSubscriptions.V1; +using static KurrentDB.Protocol.PersistentSubscriptions.V1.PersistentSubscriptions; namespace KurrentDB.Client { public partial class KurrentDBPersistentSubscriptionsClient { @@ -106,7 +107,7 @@ public async Task UpdateToStreamAsync(string streamName, string groupName, Persi throw new InvalidOperationException("The server does not support persistent subscriptions to $all."); } - using var call = new PersistentSubscriptions.PersistentSubscriptionsClient(channelInfo.CallInvoker) + using var call = new PersistentSubscriptionsClient(channelInfo.CallInvoker) .UpdateAsync(new UpdateReq { Options = new UpdateReq.Types.Options { GroupName = groupName, diff --git a/src/KurrentDB.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs b/src/KurrentDB.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs index ca4599452..0b78536fc 100644 --- a/src/KurrentDB.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs +++ b/src/KurrentDB.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs @@ -1,5 +1,5 @@ using Google.Protobuf.Collections; -using EventStore.Client.PersistentSubscriptions; +using KurrentDB.Protocol.PersistentSubscriptions.V1; namespace KurrentDB.Client { ///

zkc1?jX(9f?>=|X zt=50j1FIYQ-}K2IkaMrT_=b19{LI_3;*ZNo>`?@#VMavuW=Ef(qu5SOg5WB8ESOrP zP&A>{p26QBh+Gu%r0rf9zHytK1?r|I9n>jYng{GvDD_AdEKc!C52D5diagJ%L*p3D zQ;V(n*z^OZ%t-*cpnW0B_tY6r|N3ga&{Gh6gK3fH}q<${csv3Iv~tNR!0gZUC0T-0}6f zCuAdk<@fyHbok3o@^{Md<=!Z~C34@5*G<=4_*c`dS6nh}x%>9|Ecu8dVlRs6hqR4* z6)l6E<`EafA0?&`H^ls~O6s8TqbC6bOLz+NmS2)^oMF%RxRs9=Ott zPLJeOcj@MB_w?P=Sx@j`msjf5cC!y_*;4gTTb(ZF-=ipX=8(*^#Yc^k{33TX(x=~kB~r=F|aAr{Q^HH z3rGeId9&>@syDp#Rs?=gWa`+09RZX}kY58L-!=#*1w-+9yDVU@RJZTQPqscf-JNfp z<9+rG&)I)E=!GvrBLuB@;c5Km9e%|0q7z^4*U3I~&t3Vg_^o9F%C}wkox*$?lfdyE z{s7+U&wkImtYu#J*=IWDj5kcL|0n-!dhyAx$T!;^m=mYHmm;`Za>ZZ$x9Qr8&h<-z zc5p?mjb$NoYrpNSxoiWOSMAJU%yH~=+2x%J-Y<7k3wfdOPV>N$ctyS+*$d4cg>l1# ze@7wyJjo17wb%ebrbC#y2JwIT#5T&66-5J;_DCJrDB3Z222jb7gC4-b4PE|;?eTO! zRJH!niao;?X~D53!>8TIp`F+8Yw==2G$lszD*YqRe%4r;`l0QWHuUIJcBYx4UeyeS zu{nW=A6WW~EzhXY3u3&o`!mTmt5@c@;~OY@Z?%2{$~~P(&f9d!jc1*D<}JB;JSG8o z3{g22q@P(&0CH6hjCE3_7C=Z{G>e!GJPU#aQ=1DE;dfIR?ZmnP-~Enp=;z?iBBEUs zwFV+0W7W7bozfkE!(!1&84yW3y6_sZT-4D~0J^sF+&X8ogRW>RNv5m*HTn?OO5=~V zSGf?UpgCekf{`}{FqR1*IPI0VY$~2=Bi2nRjgnhh0gq`6Fc~L2iG>_3u^d@%smE?`dfQISMC%X@*T%aFZs^XrVaVN z^oR31>}tyQlBdwd4hox$i92n50MNN^y2RMNdEo8Hm%n;C{fGZ)z7*z6pJZR&tYBH0 zvfH;4wr$xmUH94ZrZ4^Zho%Sb$~#_czC(dAC!@bq_iWab=md~yv@_hKS)Azus@QTn zD;hVru%zab53_HTKsNr=2i|jjNn1VsT^?B7(ElzE zE#ZAo?xfRiObmZVR_bx}x!gqG{6z@nf)4iDK>YCs0yuKU@*4{=gbColSx7XbAlL!c zz;F`eEDT6UZN9G@^!NrSC|!V@05}rjkXNN}C1*gZF4$PSGmkqODufC>qMx|HxjHXB zY9!$NVUzDel1#9NzB(j1^7&DKQaL(`XH-n(FKy)%U%-tCJ|81v^Sm`5b$jgoujRe< zd`CP#0JE2F#WVI{$G&Vj>J_i`3P9`klI8z8>Wzs_A*frVPt_U(V*^YR<+C+4a3 z|86?&b#I>bOTTO%^ea{G&hL-&Q@D3dH+|^~)0Kbu-}3El*ClT17wT#*nY0vi&r1_B z=T2onHT`8Xv&n;LoQRb@p^8t&CX(H{WChF@snkp(iB}g{s*eoUh=Vd#c)ATWZGVM~ zjV*0ridVH0tAQ_(+jip*uMszp@Rl+vu{gmmxbulh(74tiD~3?eQJx#9o!E(~I45|H zB_pkn*9Ok|%*jjh&pv`feHa^T{#c*b<|dv`f~T!7vYDf}wuHtFo;okatkOPfnS6N> zdf_8WspwcLw`R?`ISJYLQ}2JzCyQY9_>FjAbwmF~ytG8aN7~Ll>2=rdUbE|n1ojDY zup7|52ok6+obU}iB3Kmsb8sQBsOS;osq4y=%_^NN7HYOQYZnr9^z-UP>WxP(R9TvRNV-)Gbk)4&zb(~O?4(|7g%W9W{r469e8p;iuhc?X;d2XsC zfYy@~%g6JEo(_*cklKE6?Aea;BkV}`s!3`XfFP?}2EVQoHGqS^$B;LAYevHqP(pcM zmxr=$zv^<|Ye)FLxjoIdrtOWwx8L#OF9+v6cI@LR{U^3w=6eN=VAFl@j?L4pmtQh%y66+roq1+qM>cm${6F+IiZXFb z1G8MmUMm6ew_IFbfx%ZxWl$g#Ak;(}G^s<>AQ|9CH;7%!2;s$8rG#}|k@wi~A*hPc z(9Ij#qFpEEbp>3qs$H8f_939hXq^}qUETZ|4}?Njds+oyIgGbj!~smQ0wpibz8U3HQ{dZHiy=e42VmRZpB8q2MK%3nw6+0KtZF7 z{n*;IyEeY-cYba4bLKki8$YaW=-;@nW=g(%=B$%nm+yYxeOMB~DaLBB1SCsM}-3c=dJi!goT4LWNiF1p`A`voD?V~1jy2ckdiD)@k$mZ$K1|`#IHgP z4YWgdokqGCi7se{L`vvJa)M`+vXg!#Fa4#QqA1Le6Dsr^%yQ9pHihoUFLvF3>&<>M z84uBI*ylNUD*m8KbW40I(Zn{cun&F7vDu(sRe+xQoXsLO&Ebv-L5vuI%BfwR zAUSK&r0cmS7Tk=G&ZM#yExcV6oc;+8`N2!mlV4Y{A*P?=n(Z<1xtkhrbmdB18lr@~ zE;djDPg{C);vZgPe6*{Euf@-|%wYjqHSMNqIY5>R_GmLZcWBxaVP#vWwMW8&BXij3 zKNJe7)&L~<&-mf}NM>c@zxM`kqtHydpvP{H%mm)+%K66b120GnHqleoGTT04Zz>E{qV(8%v=U zi9q@!ion^(rH6*(;DXA+p^w<$9S=Y^njMNB)&=9bWziKp#L}lXYN$sR;FjbJFFEr^ z;z9!o{k*%7nyUcVNo^*N)HPNKaZ!gZK)Aq`jC27Z2Ppr%DtRCSi6zTheaJ|G%p=Uc z1XMd%G;|D-1wO)_C+!lbJnPl$2u7TjGc|%^Vas^y`(moAVj+O&6UvZO*s5J^JAN@jti6i6s19^GpmHPU%xqd?=JgA!wz^1O~^v zrafYt33FbX7aDQQYYUW@K9kK`XI!cBLU4}aZOU9RkhM48&PUhaITBanAwoCsUZ>pI zLVT2$4u-N3oy%tR#LnCmzz*@+)3U*F#z7f&pk(ZD%{81pqhNtK0>8!{cuEOs_^7at zX4&amHG>Yj7~dh2t9;vFoRn}oJqYo!89Re-VuvXk$~FLs3B{bl(xwDerRL)U>(=gO zGyh6MSF3N*1H+x)B-<+a@W3zs=(}FBcIS?Nn0&5-_X@_{=Seb5?fw9rC>3Q|e_(>@A$BBW-3tcJ5$%}-5ZF~f# z?5^AH$aN!b^14E=j($=|9poQ$Eqb6akA$^&?;b$LWz419CzZ?#hu(f9$ApLv0uK}K z|7jz_;Z-+(MTKKt*d{d-HD z$MaUeZI@p*-IC4xk^Hpn_U+tSXMUM+3hSWmXt7DS*@0jlGEew<#3?QpBwMea%!_lu zoc3H5QFxiKF!4MG&IpbY??rtf}0#gl&782~3KC1A>a_)C7q7KA+HwA|LIbO_8*&@y8no3Q`_NxSAJ0O0mP;qwl~VUJO$4e<1qfX z0$yo7@bjJ5UO8R-!QY;4`QpXiT4%#_BA7e87hl$V1Fj^`gJ%397=?{@l!~XK6UKZp z0f-@xQjm%qTA3B5x8@gE&u!5ch0_=SH8F48BQr%g>PZh^WV6vBi~nvLgSJDGnpkzs zv(XVe6yz94#9Q$LHtroi@X(j;L8utK7*H~19E`0adN@SrdC<=|2OpkTUl#-Ex@L!c zu!kSoanj;?4Bny8wT47c=^ntmQ3p!?VrD$~ip`W-i{*dkR4DANzxHR`bn$a&-m`Q4 zj&pwMx8MD&|2^||)!(~X-O&Huu3IpE{OZ5n`lD}t^@UGtTgzj)uM1`a3%Sm-YjM?r z2*!0uBnPgD1azL{ZiEomP7cI>zUt70;96ik^rG3-i=$R?q$dfK4Cq-r_WICb11wxE zHWwHxIt9`6=|PiK8D5l48(pkQwl)AA^iqUeBG|&07hEMP2%V8SC=!lO{-#ovnJ75q zS>+Jkv7s*~L)As7f}yH0tb-iUPKhKHIBV{}D%{;^p`eOD+_ruE^G5>B%O~=&Co&&brhbI7{&IS&pKVP+{&zQMG zHvG@WO}RSc%5=!y-FMKSVQga=St-z10;iN5bgm7JzE`LqLt@;rIcqOBIrAPD$yPH2SUJMu@IybwtfzCX%9c0L#rmeB<7xvwE%Q-*sfB(j# z;#Uv|mC|(&?viVgU_9QF z%j(^o-^v~q)m3Wk*(4=4IEou!`tId{F|LUNWK>}3}nQ)A1XdNY3Fn zIpu_ulh4c%@ldfCO>9eM+rE?2vr6S$&5b?OuPe;pGCnckZQOK?Ij-`XjrruR%a}tq zun6XDl->MnEq}CoK9L(1eH|!pD*<0&t z*|33M-e}b$dhuCz-o~p~N4ere$BH|Hw)GEP`+?Zy+Yoo=1uwMzZoF$79eN&c8h%Uh z*1KyETe;OA8Z3x|%gHmX^GYbF8yWanv_d>H;o*ECVUZFoV zew?M8yXlfIoPF{c_hzF#F$?Sk1Hrl=rY)&W40KrRAXwyuZMuT73S0}G0@s=lkcBhO zij_;URNg`+a$Rr|wD3K~C|W^b_RcyT_>k5K0=eBRQahxxE}<__j_?k|Qmt;tq=7z= zk_0HpAalY1mlwH1zwm@Ras&jX0J%)u;7lL|$}#kmt{jUFGE$Xgh4gBDCJc_TEwN4d z9H<*6mSiXr6p$upSQFPcCcr=Lj!jqP>F)cJKzH~fCmZr}q|cPX`}O?5 z%?+QwXu9f?e>L5G-8I^&&q?s{gFg0ZuG+KD#0xM0PS_HxMGrv=Bugh}wB_x=W_^n~vB$|t>mD18MPPGVq#OF+o$bOOLyl`9 zV-YhdmkL!wL#_ZcXjM3T3wa2cv1!XF4C^Y_Eb!VFyP#7x@)-;cbXtmAVDbm<8f|W4QAMIM}1!GhLSXw}Pti?!i5r>2=dMt_nL9he^bM=IBv2e$tvF?SFY7fyxLlPOX zYAoqEq#$3e=r?2nt>KGEok)85ize3U61jD24@o_CFXoXjg!fnr8uW-H6A1~R_0nE^ zkbW{6`lwy%y0s@^#4PJtnjqVPFu9idjoQOI10U+jZ6yPVbjG(MCe;=g`mkx2fptOW2Q5xbbfaId{{g z*PV6BX`8a94#{dcdBikl+AIiy+%)D20?SHTTIkgah_ik=9LC~GTk_Dm(Si5BHXlim z1w2=WE^vXuSoD=dfz=B33LxE)c!Q`CemS|D98x**KI3Q?AYB9mXw}MO6w%N+CJK#= zfpi7q4-!)KN79;W;7$1C#9ywfF)9JIK=e!#dJFy04qzlN#5P62=s=qaej6%=C6qd1 zBV*SscA%B~C4~%MrKqp|3xx^=MiR{JNaYDQlQGEe>7d=vEw* zze8F+U7n4=;I%jbv}-1qqlycnFGT@TF6M~wdqRy1gDTW8@D>U@cIRj=0q}uKNlQBW z0}gI%^0Vy_LtVTxKkT8M<5IGsLB1tAFxrTzg>6wUoxxi&$H_<-v}43?$ryHW3qJRo zN_=F%TW_g_4*gR+qVtnpbw@2a6VHv~?4I0Ka~pU`RtIQ;{^R^+z{Y?1{&!y>YW47x zdth}#f686D5P1Hk%QnCDWSi4`lkT9XTbD&M4$%@pf3dGIG~QXp48{GkyP4i8}pZNGB>G0|CF22qgcBRzJf?0RCmU$Aob z^;hM4;%}Y~&2OM@c+PXgJ)I9*^Cd8wF8oBkZ~eo$&Aur=Vv`5nqHsN&S##;;gAX!K zJQv}K>^zS;sQe*g%_~Ci5yjeO;{wC)^IJMnh0dB|MH}%`Q&;m}^H)@m&ioDHgjf1H zL=64BGS6PgqH2!8@rLclM|Kn0s59g%VvYGtMSsW)hIEssm96p*g^c} z?J(an_o2AbvDR(rGd_`mUO8uaU!dpw#QWcU#gMyNKlL71-O!(UpY9>?!cCVw@QxE- z`Kg_|*KJQ=pOzG`k1LD>QU9T@h3&d>(Ff9tbOAK(&-28p)on zSa8_l=UTO*M#4x@Tp6H}gxYO_Dxu==MJ22{8GzJ$OpTBnWLF;k!*kFC5qnM8WI{6{ z7*b;KNCU_Ml#&FQqZ_>$biwNt4f69s3iHV(SM!{QvhvGr58r#wbi*YV`PHWfzTky^ zb{zVrOX2Em#}kiFH(v7D>97CbzfL!Q@sjC@+*+@zx|(Brg~lU(Ykqn|1CGtkX{c!X z5}}#KdEAD-ygdv)7^a{$l%gF|GJ7Zfb~6cGSRmryMK;UCJpQGrKOA@S=XZ-W^TdPrp!oa7?=%o=vU;iHavkD zI?)H8ew`G+WO+1!{>zWAU9<70fBRQgKbAJU@-3<>9==7wSHfnWGXKlbBUF+UyX z@#Om|y*SeWG!g`h%IzbIq`o_y#T0^GINnC*ffRV)6A?@2UqF!#1c9Rqg~Yg%c3vh=Vd;`I!i{axrpv;gX#%e+yO>Avij5@N zMFI>vNd&s!DDYN-2QH}K-P9+Yj^LxfLmL$<(+3W&^&R>&F+}pjC_x;?6UAP=;Sv1P zS3JWH$_spKYLJoOsVm`^Ey><>6-R=lq~fDZ@bimq$Gr9p(~&2=Vmjc^L;Vq$B3k() zF~TpP-Ibq_z3!s(r~C3zw0iF~x|E|^sqz~_U@aKQS z!Lgw)Zw}C8gqo9#NPI9e%YLspE45IHIdxt`=$&<9kpQp`L5AI(c;=~iX)~rzeWxCA z>l5<`>ryeq$jmDncEn(F&*QocdD4B@OT5ND#=#bNwR_Z&!MMENjvdgbZ{Wx+1+lv5 z99tiV0h(8M&{yiL!92ZQbiroseCGh0Q=rqmItov%#dFV_&x9X-S_;agRdUGn{a5rkY0>qd29<#@;Tv?+?+P-kpm&<`m zSI33F6`(?C##jIHD~WTEcxpt4!2>T#q8A3v1sPmnQyQ+!)*mpuA@f_oXgs;Fmyz&* zM;GR{HaG!8s-NNhTE0s5{#$QM!rd{g-*-R1u9ZZ(QiY9;-z2~5+N<)rc$3#Skz(A3EQ^lLvAdwG)m_m(8BHowIlSh?)*EjHxa zMf~~Ke(~D3o%mY51OB-Q|5vi6JhNvpBRR27fLw&7cM){ zz+J#BS{8e{y`V>jZpjtmVeUg*1YSjxG$`Y$-|K_u#8d8)amcGST*e~F!0;r!qZhXX z5s3>6fKN|*)~Z8aFoCyq(rDZip>Fi@Y4{~O%RMf=*LMy(85T_)kqyX zq|LFtXdjW-Xi0@%+`wq_*W;>pV4NqAbZ5~v;=k5S8`9?MQrXn`Y0(`|Z1dqce!u)% zUHHYeJFmGizf1nf>H5!K=+DO1&XVOP*x<6`7$dz(_&}7SA=d)fU%Q5*@`z%`ROqhvUrVgl?{gGc^=+ueTt|B-U-#6~{0rfs=uM?&bf#-4Yt z-)F};Kl%Ruc!O%J9#+%?%-U+%iyru;ANXH>EQ|47QQ>5-+OhzBuaTsX1!`(w;f$He zN1j+cE^PE3i(a$d5(yJYaLz%Jlkj-(h4(1yO2{NN@=o!17X_Y_v+r`phQ^0=U=nNlMv3X}w znW_$s+p#HLxrYurMj~gfc4c!fJT-oxDp=`O$(1jod0u`Z^{7|8W;*ISPn*_l_=kT_ z{LUTQ{n*)!7k_4YFyHdF^`VFK1-9&r{psij^z-VK+r0rX&!fz7*YR2>Uu6g1YDRoJ zzxca&FE{`IKmbWZK~y=uFoucSlu)&p$cxSR1+UM32)<%0V>0kkDXJ&InGbn7=9)!b z-Y;kOMtA5^YesD4c>%uub!Xn@VC=HBn8&!(i#rH4pW50st;-MeP_&gf&%e(`0E@lG zHS;?BfxjXX8Tu1fq(7bw&`*K+qd+jBPAX60=Xfn0qcE%>`S%;)@C4t2hdg5w@w{)I zSG|WGo59H#WS1J3fe?qhnlo>^^VUynS^Vr(GZ5p;DP}R!r&O{jSTFNR zBqY)l>6*3xIS#@S0g($fjU~B(HJp4%VSHYUsa*RDanh45!vKCH1(F0*A%|(ExDb4`NuvyUHh5ya%=yt{2c7o7!XUN6Xzy8lv#e; zoiD=416R>sAGr$#gTX2Kf(@*LgWK(({QBO2>#e1--MP|taw-!T8#%E=Oxfn)GJKsw z`PzY`asH|+4oND_am0Fz;A>40A}n@UGm#mOlUg)de1$4K2COm5xY&y$3X`!*Cm8*h zTHwe;Z~Ga`jShNj1Q);5$!eGaqxhbBx3L%dTuZ?6qCbkZCrI!oF~V58ce4=((Db8L zPHg7n05k_>g$_IL2hqsMizhI_5C0B35e)Cb#|wQJI})`En#}88Bs4ev%m@F|AEvun z_M!(?H}t*e{4w-}*IjnQ*(bgF;#!&0nI?fCS%jn;7c&dkC06iCPPGBlvWG64YWO?> zpcQ`CI#%af!0lD^kK2hxQ;w8Xc>T-K@e7ul2PLV^J+IJw;suIfT%JL4V%%d z>_Wld<}0`K;U#IP3upftfslxzU5(N`eS9Hw=#-BBksk3V(t0#ZrCL;pt7M6Y8O&IN z7ouRs$(5);d@zZ`xgahFe}$yJ?cs-}YcBkY>B}Ge@busvo0Hsg64Oi> zF)d;sbDEk>oz2fHXKbZdmF{cjJ4c1mjx@D564=b6F_x4+m@`#`zj?r=gD;5(#53mC zE5$l9N}^hQl2>tBoD~a8v|OP>+#BA&I8qMa#HU{97FpRy{gYuyFXmBPXgr}Q?=voU zWv~T7%Cb(s$3a8Ja+3_q^$0L!oYc@BqYeL1*U1Q*`_Q>5AueEjZ$13jGTVg#vC;S} zo4|WFB65em=u{_|ZFg+MGUgIJp$pBg?_NJ`{NMiYH!cZswd`dNFhi^5Y2<-l{DB|) ziLAVzh)TIyuSGN8f)CK!&q+jUbAPMSZGfSxghHYqD_{_da8X#}yeduBk;OXSGKCld zDi*%d_mlu!`MFqOV}ln86G3X<*Qc+XN{TZNymMKW8(_L52k%ZqCqJf_WHNNXX7Rux z2fvKOr3EEZJv_nPb~#3P#wQm%p~FUK9hdyfM-0bChYuJe^Vj1+ z%g>}9^@`U{-*L*Trsp31BA<$HRpyGXn%(-q1Jm6%Zkles^mEgL_w+}D$0iq@uwV_h zWYL{XezuW`pQastEGje^vwYcpu8G&j+8*L{xz)}lmo1>?48^knx!Gl7a3c$yn{L~J zab+(bTf+wxr^bgHc0j`-aOmkvQgv4QN{e`tjPRleqrUzy_skdKub>*3>9=J=5ouiCY}Bx9drscS}T9cs;C zYi4u(y6bbY^6NkQC-3`phFUF84-c$fp+7z5Vb&rK%w2reNw2vs%lP?OPbaXjSkNX3 zk{e0g_gY!n$qT@`u>ROA$QHJNr&~#(=|r%>u<*J4*zokT)aPt5k`*H6QtLf3;faiq zz|#lBI5bFKjdBAtu{dHjfZ)9HmM2UZ-zx&}v{RehLr2^H*e?x(z|R;iX#7<o?XkDHqH{W9(g+MaJ)s$s48Gjbda7JRlr5mSQq{J^ zZhc)bIR2}aW{H0E#rJ#(LsnQg^pH*wI0|MAxPWv(A!pu-!^R;vh4H)Tcw^ln#PJcP9#AoEX=1G&=>y8aA7fdw=A&7W8-MoCf9v;w zu9l~d2Ua)qr_Wr>np|-07jHQGG_|HnPGyZf<%JFs%1ml6{4H0>Y0EAyA_DZKesuURjomPz z3jsR7KNv<*i7#e;VbXAbwD_u*W5!;{XefhI7DZe1d~f(S z{@h>wzn4k0dU#rS;7Jkmw9;)=nRwure(0b7vvs@I{EH;P({Koh)^BK{lSMWXotrYV z35*p^vEty07BnX?pjq7TgKdsVddf}0;0k_;d#2BAI^Qeg_9s_!En!^E%@5WjVURq$ zLNIZv-aOhoE(s4jNw96I6O~w!D?4n~D)6mo$hrh0I0pn$kTvii?RVYi1u=x7=U;vg z=~l7>$Y#scuR5g9ZA2I#+k_P4V_B4X*!>>8|c-S@OiwDKZ+;FEMe93-=_ zSJvpGciP-N3bSu7Ag5pzx0z7CQI2^P$2eU^T*S00(QDoMb(iKu_&@(2=l#xq9pKga z>F0sfEA*$|oa~{|`I|2N^4TZ7_VTRyeX_7$dESm96uF<_V72pf~W;v>oLwMboQoB(k z`B6$*iPj$(LVG}>3a=1(;)RUXhYd;I*f_pCCr0!p2XnCMNS%K-0DJ|wVX3QM3q0~gNl?tg@P{;kD+fB`i-AgySzgOmPRnOJ`#Fw{vF4(EjY=Va6*@Is<_*A& z4je?B6tMYmGFyVfHySMnkwK1^gz2j%y4y}_rCF0AE%M7}L=B~7*rZ)Bjuhlu_o3`k ztS496D@7lgp1G2f@j!WiFAe_bV<-5up`~3LISi$j3PooAerL_@og2?N{{tTbyIP*U z9$4MbpT2W5Yj)nIOK<y2y#l-sdY>guc^?q_VU~Dhl}p zxF1tOe(PyXLKL6T0v8?Pp|>YC?)vB=VbNey%Ne!kwiP08rK?a`((LNp%BMRa3-VXX)7t|h zKAzrMu4*@X;8%X+e?2RU^~dwR){n5}tE7>CmM{yI*OO3^@m?4Un8ivGGm(*eNF4Pq zH{B{jQtIX*5wiFxCWa&f`O`N?Hg2@JO*3|G%ifo{K^OPql=n0P7v(h#^ zu-XWyL*pQo{7R$Jx42QO6K>><+l{I>co_uZ*17bojYNM0E-W^Z*wNBk?a~Jq7!FlAHu zW!6dK;LCVRw_3-^i#)*5hpW}FgVR>7T;Ze7AAV_l^#gOrAH(Os#)1$0%yn!W8@rnw zG?JweT3~KSk?qEZ4}(H`;52C4q=D~K&2=?_6s-Bh)8?NsF^IYAHn-J@WuA23%~Rja zw=18yulYn=2vV>u3(XcsZ=+ra~?SLojkv$Uk+c~@U@(_2qC{epEH)<2j9e0&!5^GOmcW|LkP?MSdD z8F)u>pxs$R8W(QesyC;Mgae%gKp6OMz5 zbWFz6hM_NcGJhsNvtf;^pwe1!ac3<6pP z6t#VoC^;HW(#JnTVr^h0eQe~+iyK_OrwSGweX;p5AbK($8^BT-bmBurvu-x!?DmGc^+wWOF0D;B-7Y5m&jUY{8_IwLTlG12XKK=r{gAxe7areId1_n zuV{p>#&h(p6E_SU`k{-&(m_knFEo)4oiLQ7Ct&WnR2M|izfSAD8 z;3-BrCu9+eYJ6MQ0DZ`@etJ zJF`yz=d98nW>K4DNCb;j?b@#lP;9R6o^`LTl1TPQ(jl=Yc|}AM8N!B&Jw__7&bg8q?=5@f9u9724twfqhAw=06+42f03Txp zQgrA$22DE~(jNT5&G_(0%AtASEy>X%I-?8yaRP(h_<$=_H$UuxE^tp$(uD{4p18puU1^srQEf}{ zfgk=Z8Z3*wsnjJ6;Q)}wOC)@DYR+PZ_O}meE_0=tdE;R@p8_Yqn-^K=xm?p#*I{Fx z7pTvl51T`vt>v3K5vaECmWdp6)JLgH^BQVCqKD&`9TgKb7sDUW()Owy@|NXSy%MY7 zy$i8yXWBd;vU}G@BL4sS`3ryl;}WhOzU@4)dWHUNH)RWjKXKI;Zus6)-f-UTwL2e3 zA~-f#>=0AM$T^Uj02Vxgav))l5JufB5m5=qj0hG2OC=$43OJJw+28$EKoFIP(p#`~ zo5V843#gJ{2u(`a%;DiMU!h00L0?y&OE^hkhKZ{Fsy{0%zcLuR2&L?1jZJ5dvf$U6 z>|s5Oc^!G4yo5)}Av^s+XJa8o#Q)dcxd2<274`kQk8g&zN+3i;f{I8K6Eq@XBq|1V zG{{3HFoPO2RhmkQc_3h7DwV-16qyGIvC6VE3^T$=L3lWk2?HXE5loC=07V{32x5rD zh`frK@7|l=@88|~oI5ZnL?*+0v)8?6@3ngM>ebzApZ>44_CDv>&cI(TgHyl?Xt$sS zIh0!Tj#{wV)-=A44o$bFs~7#`EuO5ns{Jr|RyTx2p1l21kaLzWjQ*UHmB|`FA)r6? z(mOwZp3$(MY(2NlmbGzqBkaqzIe6kiN>O^hmTDQdQ;+9Ds`{*gdpy)p`yz|8DpuJ{ zz37GPR9f%kjJjo#L!SEZDYtbEm1gOWg1QIPJ=1S~7hK*HT)7ud+B5E>$YQ6w>SRwa ztLO35I>J_b%B!)pjqH)#a-5{7+t{B*iqHnm*@pM*q)euxqjz!J;Tij9UjNjmzWRl4 zIo|hLx+QtQOJq%_fd?)<_le)ky!@YF%HzhI3~t`!ylHvkn=+G^W}x!yo0^jxd2@SC zJpo~6^ag@7H%O3&+pmo@WwlE1$Tv~RW5A~>D?2mt+9p{JBPVCOqOC8gvT~gjxiaCg zH(8gJ4fJ#n05JMmAC1U&QXbjZN8e>)LU(BKL0IK7Rh^8d219lF`=(3O`ox(HIz zQlU9+b^o9LEt#0G z7xzj;2N`BOG6~5bREy}H@hjWX1RLs`;*_Lay*l@NaPWM__~(D@SFZTglCU0c$sSl& z=(pt2+tU5oH^2Vf-+tc*UBi3(Pe8}dXVs8j>*)7Cfn|FIWJ3app<;<19Ys=xZdEr< z;GsOhMDR8ENHUlO#3>J`8CZ?k=}}((6v)>_CO?6Vu-E+)eBdZLXA743%V&GdE6{{a z1@1^sqiW~FNQ+MBQd^EK*LJb1^src|eW0DB3X&z%{wyog6T1}1A>Rk7q$!_F zWSCEXoRlLdM2>zzmh2t$r3VbUoQJ4d=J-SZ+ot+aRA~|GqB}bB5x!zbSMcclZe`#8 z*=}^9Hq{Y@Bqs*!Mr7cW=|q5&efy_fu}z=AHv0P{I2Bb6U2aGm>KDdXEehl((JvKs z%GcbgGo6e*t}4wtcFK>>!@;4GA}UpG(F=QXol+?fm(1!g^0cGn>y^34@{lLR*rtDC zyEbliH)^%p_S;lBEz2I_3(BT_z5>>X>GU5zDA$CJ`KC2^ec#li7ax;1>8JREzEvZK zMONvksKRUgRuc!?s&9>trD~c?w|By*$T`CNM#lVUclh9kf9ywp{VIPPYE7qs2l`4o z4J=trSUhm)W1sj{{JiN85Q4s+H~Q^)7jF`8@LG9GG#K+HH%gH44PS|TRt$nqg#m+0 z0tRIPSJ2M=RAP|d`@OWL&bG|`VL)!8kIc5OvqvWw$`c|d=m~bwg=R-=%_d;UOL+-i z<|Rx@z$#Es{47YVR+fGEDjTa&bhT4$^Aux>Z&XaJAO}HO9te{iLao|tR-mVi*}e>C z_i?P>x+iRB273c{o`SY>ec!g)QavSk*yvoe8``m{pFTa0FF4y@gJVr_tc5OspmY8- z&@u9CYx06s?y15NKI(i5+T?hh^8UK^`6s{qXbQB*CLcTddjh28zOptUyTEY3Xxb>n z%OZ^tmVY%MRUesbsJFW6q&ZVx{+Ha7xv95xCMSI@DA9lHDcv#Ay!LpmRcm^!6*_%) zSMk5LA}^DVT*C##xptkn_=Q27%M8kVv5bI?i0GV2hqfscYR+b3Qt22P`Uv>SQcrCi z-q;UG#iDnry%rr2WnvR{Oj_&EyIXw7x|MHi9{*A-4o=*=Ccqy&vH1)v6 z=Un)Y_I8dQ&-?u>gNJYUiZq-7PXky53<8-x@Bo{k)zA=r=|l(v#1d%ym`_%O1`T+r z^CwXA9pF$5K-*jHP5=z}@=ZWf?1UWUPSBSgFgK$n6AT3c{z4_6;A%Ug*Rf>abEP8| zRWL5&kgNT}Za#%RQ#Lm?n@=Fk8BOgc0WkzBONWDi1^v+8{;GY|8Tf&38W{BL7&@l} zceQiZDqEJ_6v1Li za(N^@ucza>q|7z_*D79HU1-O?$P)m8l8}c&R;ZD`RZG`o)(3 zB$tXSzu~aT@u{-P_A!3Ob{R9BLMq+Nc@3*KX>4(FKyjx5JzLSp&-F}&{JuxhmTTVR zcvT~*{AFjz;ll>%_Rl;6mGB^W-&?tUwL5-*A3E8TBe2vgkK;>VN9!8m`I9|HaFGX8oDdap`GxUboy&vjN>U=T&cc!&}d~ z-veKLw6pgC0>C{8BzGZVgvIx{!6q|#3rPlyS(QUD@(?VRWH!O1f>MHL2a?$)52#VF zmjHP+X;`y4e+D?^c92uPqG!u=18^0}D+m^UH6xE62EPg9%1a<`z#|gyvW!BI!UQb!w#VE+LI}ERyO4Ts*S`>0l>!et$hjnl;eT)MV%{j{{jlI z54~kpaJQqaS6U!PnaEDE9=f!~v+M*YMth`HPmAEllvb%N64YQxaEPoljC%x%=%Y-T zT2w^PM{Ec?GPNUMR=!7>n!G^w(+gI6sFTO2le0F&cipv2xi-6Ow=>==G*YdC8w@G`P3hM`A_pR&1*Vc zJup|;>1y9Hb1(eb!=1x_OsM%j-sN{l)FPN{0^dZ78@LSe1hWJ}qFrYG!)7q` z8?`FRB!Fa#lX}1N+$5M}MK}S-sF&bR9Xc2Qy%1fo2AT|hvyuzlz(- zzHHGNSl+a2t|^#HmsUP1ZTwHMSFSaFW2bGBvKl8ZpkrgMSMvJEugaCpF-*n=^ryDA z_M9=1=F9Qqyvz5vd(MN6+S&B&NvUniZgZ6)i|p7dHCn=wYt3^A!|$1E65_P6x^@&w zKi;Pwrt%8T&-;-VJ!b{-a@}ct++0zowVA7NGJ|cmD;4=Y%NR<3U zEH4g&M1YJs)BM6yZ6|k-5hRQ@9dVRbxRDUrLIlEyr~oJr#R+5yfQIV^G=a`K`}aIR zr-acfATVY7hR-@xHCHQ~=1;Yt?Ck>4M|G>saotmuI{Tiw@Q$63l|9?+ib=g-m`2r3 z+G0c7MTpOYAfLBakDj>5S3zH3u;UnECTZljBD9lIy~hhm{L_6eDyYmhjYiQ^*~+Lv zihKozRgg0Sz&T#>1SCA$Ou&mA>cZH4l*l+gwrG#K_BhTrsAcy6`i!S6ipc~ZdMU!T zMOV3v=*pFz=_+l<&ohDY+QjqxsXGF4VC)q(_GyEEPDZiGvCMCxciGztO7ME0398Lo zYUQG*x@u2>_wGqn=BSw`-7~(9Cp_(k_=MIqj!HBs!&8l?S!JN!LMvZeQB|qupzrEE zS^nA@lb2C_<&$3Utd~@)^>}OWz`EsrYZ&D%8?Sxy8-D*=9`N8->>V7wcV}ns&IGG3 z>=03bN8S!52{Kb|83~@USQ$hnAj)q*O8_zsN!TQL4W64-81?W5<6z4+G-XAGmow5r z;1~d&Kv;lJz-0B)-EDOQuWS4S>mpaX4wTVj6OzNB5R;wv$*h4g9fWGo$13Lm25Bng z&hCPJ2^b4FYKV-b-PojXuE$%82evMUTZ_r7o#TOLJmv{svcJ3k zIDqMKM6>%4GL{Hi0pGGKX@e-Oyjsi>7%NdQo+fh2&{Trtw5ioiHH=K#Qxvc>5uk~| zu2ycwFe-hy9c^6>()>3FR^Am~w*Emf>BYubeR+j1{MM*j1A-;$XL1p(1>+LYwiJ)D z)Ft1nWx#dodCtng4~66ONj~>uX%m0YXLLvhh)KXk250lxkw~C#KhTf-o0V%TB%QuY z+j^IMs@JuxYw-a>0E;>~*U-=7dso8lxJ1XMXR;Fc*r1;wBiSY|@&RS)Q{zFt_G9d@yp`s9w4F&g zxdtpO<6*zr)#DRh^8NFv@*VKq?ee%f_h(`&Ikt6;SruM69 z&5mi?zNL4@xh#yn(-)Fa1*ekqWo2qiY#trj7hZdEgm!rJb_&lwb!z|lPkG_R@2ZCD z@z&;nzG7}|X0P^cd*HIiUidYK`zOxj3pUOrJlxIjWq>m<8Y~Qg+i3tU^@96Wy@B4) zW{}N`rUTr{F@9%dtH4Ak`Y|BdhC0t-l976jbv6%Q^okIp8MJ-{pwJZ}%{!oT9=ZY6 zr?mrg+6HIU*+I2S&Op!0lT7j~w;Tj?E0oJ$1%3M{AH=Ra4Xy6D1^x=`v{84GPpjLX zI}BlR%N=><0}^u%%_{3U$;zKPjoniv=)^4>)_|JD*b=>Ij~+EPGrncxfTGMktan`F4|om)cG_){*Um;y6%K%BJTdd+ z8FAeSfgQoiCmFP_uYno68e8=!dC`?VrBt%L*SzZ5Z?YE1O`D_lqwEU~cTT+EiB~@J zx4Yqbx|MpsYh+EU2jYQ?&$-|s0MI!=)j1Gup8#TjA$}NqDtHr52zmy&oMBv6rsSp7 z_OFXKBaxzAn8$j2N z1jw{&9VbZAcIwF<%Tw6;#;t7-DS7b^AV_dsCN&Ajlmh|^6xit3)oJwR99soA6^Ao{ zKl!#z-w@X`Ign*L$)^eGhJe!Dc12TGJ_hCdq4}0GJdPEkS!1bRtaRI5Ko;0Z`Pdp0 z1}84DG5n0_fT(hz%}4W8$@c2&*gDaPO~G?J-K=P3RateP7o7yi%BFBc{^CPP@N6d^ zXtUo;u8tM?$;&$9}8~&9<@2wwk~Q=d9jds~JB& zwg!-Eub)3RIf08^eLb5my62XtJoQ9I>0f=t#2D}i<|^VN$hwM&+*KD|dC6-etZDT? zUs0Mw64CZ1bSBsv2n{j@JA=M?gFSx%@-SB*r7{82uvo#F zKqb4p1~y-`Uml3_Y7CuV$jWtspXYj6UEpoIJSCp~O814oO|i;;J&&qsZ(I68?MjqW zaFrF{vJEtCv_I?9hJ0_uW=q#*@a0jJ&JZfS#=O3q!5Y zyG|LMmKCouhsZfB9&SL4zSPQ%)S67uUu*`nj6H=8 z&GC-C$j{`nWhpl@;EkrEZ`)}b?W#&2#y|Q+TDq7y`)I34pF#2rlMp^`egmG_L3-5qJ_@Ip7%Sb*#`i(VO1uT4I7N$0zM2Ax!^bnSK$C!j-%e>l?6wsC&v^S+VP zw#j3CP7o=-2z6h0!SgQu_sU<>>VX@2z)NaPs|Rk>1ICj}&N&Z2-#wdX`Dmd~z!A_B zk2r4vC)@;7dEDka5F|Qi89?(BsRqeSB&+(CNqDTX^#(s~s{8%#rdkClY&%s@H1IVn z87|YMT=c#qc?2N?uKJG7;h{V!^dg27ARt#OBr3GYGQc(;TdT5kf^KBDS+Nsp%;8`^ z1b1zbqaF$~(4%t$o3b{5zGWY6wIS9Wb3^H4yY996p_H1mW4!Epyc8S8V^A$qzxc#9 zlC~x(f?g!BLt*#vA?>75O4reiQYI|equu87ku}T|nF99uCAL6|?PZr_Dq^Sl*wzV1 z$R5p9hcYhureL{Ksj;reZpvaOG;GbS1!#FF4tr(HF2q*H$}K(ZtaP18 z%Y0NVD|4B+c4M||A#4`Hwzzc=Tw3)T4CIqQNtz@@sEAH4`lM-}onwfyNIu1x(~TFJCg)*^4Kz~bK6uq+H@ZZKK3kw_M*Qao(CSWaX%kJL}*s#Rj2Ky z@sDapZ`+%X#^_4Ht~j8~&WpMA_2Tm{zxc(lglyHCRu9~m2XZOhSmjDzJ@CmqaPisa zJ@RPp#G?p)kK%_}ZfD@iLrJb`3`|5n18Zmm$OI_@m7q8Q&3cn%4mkq{+Yu(TZ zpjqKmK$JI=1cGe=UCCG0$TUB|F;UP9Jvv37>Q~wV#_2L#HZNJRBSpZUd#mUqhxgWD z$o=zW=}z*4&n5v`~sb+N4{ zbt{_(tYM)7Gq8m*DHR}$cfVsk?XaNYa+G5(+SN@Qmm3L~xz(Jq_ERml?o`#(* zYZtvBTU`gw`A=EvX8ateva`zO{)j~N*;Ln+5hYtc8_)*!It{N36A$p@B`!u*DDZCK(wO@ ziWPcYsRUFFh6Xua0$xCkh-bh{nGkOft<_&$wiOV!RJXmvv?Ew{o9WwreTq4NBV_O& zeuK3IlkIbYRtAK}-cz*R3}^K&upc?}C=gJODkNJenR5sLFFM3F?cx;>515g6MQ1|1 zJafNaQYHk*&PrB(Qt6(o4AH4et#g&)U+U#w+L&LMZ5kBk3~^TPDNyH*1nHnx<$|uO z;N_MDd^Jv`fAZ=>@~Q8r7Q0mwyO)o0$*(Ur%3JYr(cuWtHw&MppT*6ez(nj%{YfwZ2S;)%O`EBa^{( zDbD20iJRbFW1cfzd#1B@o;uul`QFaq%g(>z(%(>}HLV`_bUo0Q{ioY}ZMS-0n6R-`S7r%jd0APZ&(b|Bz$W287 z)CAaoSU2z@x8F<{RH_53kvll%xzHh63{cE!c&Q>2K4p*(pj#QbO{+flmMLkkDrAx( zK#^C61ab=r^72feFP=1O+5i$DEA>oT+?MChFO|tX^ll%~bx8knD`2+U zwYeUQD;-YE3cc;BNq}}orQc%G@=J$1yDxv@^M3NJTD7Lt1AkQyc;&5W^}uJ;15f*| z^Y6C1f8E0fnhztU@&eL~t6XgZ>IvA;0<^5+fU?rTRgJF%K~L0JCfTMlVKL5HpXh46 zl^g;9WT-xtsy#q#5RWXCn%2dd0B*J$2LLR)YNd^3S8aluOtru(kXzzD8UF-@=n`8{ z3_G$Sr)7Z}y4RvMWe%!uH?d2*-THNfr#|X7U)~bN=%_60eL4;cmnXNe0VI=fIWqrW1GH`=GZ#)$aJv4$uQ0bxFyhj#^Spuri5)rrNey^oxx)*Oro@EO#F`hNg`ff6uJg<*n<1 z`fjr6kXhLe6mqj+lfWyePo`0V)3)jJ2LuD;-^pL)Mqu4(na zXUYSc>+>@uah0=rV9NtP{_r3A4kG(MBs6{}!S9|1I0LB3i*EqVRc!JT)VG4=1jSAK zHb_Q90bxhm1bBn6;$2;)2eL3HkavH{!)N{IQo+AuCeTx_Sb3BBya98l;i>unUdkh3UOm2QT6E8CfpW^y z(4wbR(YI=%XB?!2DC;pip0{l)a;RkS6*{M$$w}HUInky}HhK6lPrK9CVd!~af;L*e zrDGMMUv2|6p!*TMjH@>IgugoO?Y?J!clVe12EqUE!&hGRiU?W{s|RiZ5BN4&)9Qhn z+yhU0fxN85x@pzRAMQ}4C5Y2d0LW_DmAPQ15(ayDnoqSCIkw5u z;HAR=m0PkJkZou(%0$r(s5fmUIWmW*^~~x#I;uc}l003XQwnSs15B=R1!Comu2Dcn z^dF3qG|SEjX+)f8o*lP|a3sndo5`h}j>(xY(k+XlMr zYN8OP;;HVUUuJC8tQcCB7S+V6$k8`r)V9P-?a)4XIFOBR0_gn{s*W-C>Ia&W2h_Vi zHI!d7CC)bb>_j+0+x%4Q?yvDRg4b}t{PL4u`0TgXVoj?DZb}b)!rS7e>}-|2df=jm z{ow7++}k~idzEMLWcIfa6z@^@S&5tgxdG8wn?Om_tb7G}^8o|{zja1i&H*d~GFLMt z51rD2GH0`%x{YW1Fk%NY?}GHp+7Xt#yH%HmXzG8vB$^3a$FFJ{@`{Mfc?Dkd6 zY%Fw&Pbt?>#@UhGhv+Addg<5P^QWz{%eJySa+)SRV|0o-{Hrb6lrPaCFRu^fX==4r zAg}19J^j)SY^=|r@4T1m`jxw9>|OKGk6m}oMOXjqN85z;w0hts_rTU$<|glJ?O^r5 zF%P(ECz5^}8F2cd!?c4|x?zKVw#%4tQa^sMWLEA7sPVK-k86N2k_GkBr2svJ*qkv{ zHyC0U{m^~t#@6f>;Ai!!?>s>FAu-@oeBCPD||nm2ittnOuj)b zezrq&Sn{;gouVpsz%769JpIKMqx@t}T-#y@UyQb9xH@gRZ@{vFEP{RL1 zx!?LwqT~G&Q3b&cpaNfGSAzI}v5MD4%#r^Bv7$;h=+D`b!9FrsQVL=NcA;8nC!X`~ zq1w!rG7%rNfLqmEsRC>!!9=i!LY4Q-t6Z@knIaX10la)h8L*jc_<)4=B4<9YEeI;s zt@hbE4_Mlv-Ij1mWuwfbIwrduM*nSlWlT8^DqkofH0?CGy$(@OQ?7tFw-<^Js&?r% z>@RyPm%aNw&}TNkK1K#V_!odDpK(NfCUUVNy+&pUEdX__XuU})L&}Nvo~;ibeV4t} zqVmO26A|*ZNjvg=3n43Wz&$)pSoo`-$St|m!=@i{Z$kf9vE}lrfAfl;dzJdEY4yO( z>49VKk(*PIweQsfdf<|Ce()}bN4NbJP~#u4Km8B_{}(3|H)zO{6`g>wl|t*X{03}k z$#3xmgtp4XTEYDoc>;a}AjkWtB!fI(E2a_@2GHxisw+OhPUsuZiw>}k&H-s{VRb6T zbHlajQh*PVb@pRkilC?7%9ym|p(mwIJ2bs{Eo{-{8~f9W`Y9{fXAGt+aBJ@3}W+Km99Zq3%2mQLeu6?|+)$SyPN^P1+jrK&X<{SR9N!{CCeG)}WZ~Lf*O-%n) zmnzF?}5BWZhkdZ4OS0)q6dEBu@~Iyj1wmwa=5qi%>d*#5&!QYIPC85 z)HDDakVt6WfL=-I;2W5!;9g)sZGyQgU%xuGR=^djE6At4JpC$!Kpy|I1uv)tu!U~p zK9z!*KyDf^4_#nGkAT*IwrCbmw}q)kj)BL@dCGU18;&nub7M;@-O`>Rl8!_2GDbhggU?WRk_yZ zc0=-d;fIrh=;kS`pP$uo_wjJ0+xWBS!f}1SU}viuHF^mj ziwZU$r#u{{?9{rGP6%4}6w-px-{9rP{4( ztseL!9=P~>pY%n$M~4rl>cPOkg9-j$R$$Qpo#0-V_5fF5f}qb2cl0(eNU;F6E9_Z0 z=6*ObvIR|WcO~zNO^I`_Jg{Ov{>vM%mnu^MqdM7#^~nR)Zn@94bUvFcjaT%fxxY?k z3cvE@=M^2$F?8ipXPf8@+pN6PxDW9Fyzqr&DOThb|ePX;*%2%`J445y-Ven>bXyc{9m@HhO87Rf4hX=?VGM zj-D2qlvWcJ+S|sLytcz4|x5rY4yM@)&rNE^TT)HHuN_D6n~G<{q=B$OB;LU<$aC&l^#` zkW)&19v;&cbP^EN*%voeOD^59w}pMSB6bX19D9)z_S|wJ%Ep3R;vT*_I)b0 zce~i@N|sD1grxGNpsi1}IiH0`k(tDdFEqIFVn2Pg3~8o0cYzbK`OCy)46Dl2(V}#5 z?4+pIjhqAW^5u2-n>&X)zsbt{KkXeH{Kk{6{@L}Ocs#crZiyZ^_SU#18nEiTdSKfF z&-}h8J&5vyz=f|PvVR@y5D`w2T*b<4vf6EE z1WWS%t4Y9zE8Fd^)x}&_#(qB9k2vL{Yks$RgPyC|0yp|VPI@)g`kJCanx zzN3hf1=>KeUGd0Rzy)LIPH6IA9%oZPEt8qbhvvau2I!lWIC3nz!;yCIQ})lkT3mUl zZzs6Qt_ca?-n2|++5jgBWw#xc4>K9S5M`HN>pbo+i&Vfio;qVeeOj4-wlvbL-ot%( z_&*@Mf%-SF)A0Hyzv!o5AEx!Ndf+thfbWkrtsXcnJ@A|#c*@=RPVNU1=D!vL9{^}P z0N}Zk;1C!D3j_^8%Qr#-|KNjXhyicvXVvNoF*LvR3a9daHwm~d93bsxn)U}gz|1#;&1pdkn zPsJA=0lhd2C@H6n_n5Vq|M1(!W-hYpuz>gMMr@uJC_9dUI;|{cD;_!K6$jOWop3f| zo>%f@w8QeG@Jhxu@~W>S#`Z5D{ukJM{pafsuK%qc`Ne15Js z%j$to?ty20&r`l^XaDf;g05e~eblce;D0sI|BeG>1MC5`00VCifdg>@dJse`VU?cL zh$3L7t-R7a3&NcQ!mKu-d2iYu@W?%Re~l}^Rjcp;x_~;(Q-_QxFM!tlMxO>dds9=t zNTo@oOI3_2rCUnqJV39?{!tDfHg}#2;Gr=egmd>nfX<|+`Qj7%rR=V`8oT>ZtLzZ! z;lG}oWOAWhC>Wb;LBoJ~^3d^-ACb$q=&Ad5LeHcd+5~a*W(xHo?i&0KWBWUYJ3GI< zd${{wFZhLL{6V)|Ppbz`YY*J;Epu8{d~KWn00mJ=L_t&=yBfE8VA}&f_4p@$DM;{D zAlv;9k9O`482laT?+#6bBMERhro4bPh>?^hz+LSYpocb~$U7^mbch0aK&ydF(1Y!S zKd-q>56}tY4UP-&1;A|3Z`YL5TOCv~Rlt~R=$)0WKr6aKRQF6eD94U)c}RoM0_YlU zyRLWglHG~s^Ogk!J!3a5n=?kDIsGz8vCqDxklPCsXUm;gLG3)y1V-#H{<5{yQEYCD zqyrgcZ9A&O(f*^w`|$toQhF1_H^KPE^Dn>n9bsD!s|Rjn9`Jp;rqu(h2k^kNzyAq$ zJvg=ZmB7j02D84Bd+PTC{JsJJyL~|%OaqJp3O}FaJ>_}$tyZ{hvlnOzN&}v*LU4+0PPOm=={t!ct$w%h%DwtWcb$g0Fd!7pLKJS@mVaDujaYVqzFCyTdj41iOo zo+kCrDm{iYAvLt+mV~wiwxRK5FCT&SR=mqss~)|1XJ`Mvq1l_yd+tyCX*XFd&IEDg+9W0=EzP z)mFKzy|d7L1eEPV{o4lHDl)qc-dYIm_E*0)k3c>Akr}%pCiXc|am(D^YU>=^{OZ^6 zxgsa)#Kf|?aIS54E0BZU)chH=w~>1rru;s)D1MKP_}{zm@~6MsCTm(fuzKJVJ&^a) zCz4mn>Vee*H|BvKKl=%H-9I?G51%8y4@+FO?RUQP;OOAqpwX9rbh#%lD0;u#&1r#4 zq{`FfWCLCTT3#Yp(+1!G>pT!waF=7`m{S(QIVL~n&BwXA9vQ)6j^cR0nRu9pg(cl$G;dr z{Gy$soqGZJ_hRe%Z?Xk{Pqw=635ean#(oWIVKdN0xd4!SzZ(Pm0x`8&)u$1ag|}(D z$ycLg+h1N#+xz9&zUOSS+y}P6VD-TN+XELp;_;t*a_2Vu_WOx1;FIWIu(!8+cL4750ocDm%ew)J zccc8*$=_*@)ofsxcc+jaXa^Vt>Yy_~$$zqS2u_C(fJR8IvYVdrhBkGO7rp`K^xI&q zz@x3F+~qfg7ohhHIRC}r&cXj;0`q<*AMdBf|A}sY!b1IhJ15Wlw72SNvAK?6upW-CMjxmm%n3&j!sX}zqY=gk`CO{yXs1ik@V~Gv|AqmlC zdKbbJ(UHOQVtPj)5WUxZlR5MDjAmDFK}d+==zZUQGjC?@oHO&L+_`ht<@$~5x2|{I zamlcVE97@B*B36At4I<1z1Z(vt|>CDcyUjDQI{+I4=z_RSw@OT2|HZ|j!(%ywi5aM zeOI=Rs^ja=yNik+hKlmE~0NpoaoVPoCuUMT^o;9W!rxnEh-fYwHgNW6)md`^pamReVv#*Vu?6? z?4s4C)=;r=V1TKb`825Q)PV zi&F0fi;;bkm3;5k7*}Hj#xv!U<5r4qzUUzmq8F?2+pWnM5!Pt5DEHU4qC-%ODi>U1 zu#yid86ld;u})cpwLa^n-m87mSnBgX_~5SIQ^YyFP(&CK(C zH}f#|hIX5(%a4edWicP~|%mL^V%c8&>YJnpTWbvcR7_a;dII?gu(vSK^1aiq-E!x=zt?x$jsZUuUn! z5y2C5o{5b(@bN64vssndxN^6ZGG~j51OM!aOf9=+>2`rM$N_&&;%c2Ye@dF*Hv#_0 zKFK-{Yl$x|--+*byHy$BW520cysN?jx8_v_s661u4w|Rpz{eiIdY1jLNnowQJMfn# zZ`65+bNyg>hQznrmMRZ?U``l3U*jix)@I=E+Iu_}kVW)a|z{OH?eHy(Y0A^4or2{5c7$H9mAR_CLzCa+m`JiLbaxy_S9B zGzSVAzxt( z8nslff9YxVxd#{z?mtpzOXB0q3Jm0>$@3!HY=*hTFE7u?+<&mfa{Zq;WP!%m_wz9o zC;m5|_t1GxV?Vd;KfKS@Gk}l&hMq%woUhr&+3~hM1bpapY$w|9H1?l`4}A{079O0{ zQ5N{?QtUAWeDpWYK*Yyc9dY3A++yn~%ySz1$YcNCyZb=NLjSpb^|rE20N*X=u+(QY zK6Pc_@7!`or4ys>m*E;8`vB7MTR3f<* zXG7qd-xPKT;-US6I%l7U$3)H6c`U;a?LTq&yw+VMhE@I3FpVD*K2yua4YS7@WP#ry zDEmBsb1>@!e$z^}E{e1+4MyrbmSNafo*UX4x_b9!T!8Y_ZxP2g;aM2( z9P5bJC>~~UT&S|1K11C1S4yrOW}0~xGvg@Z`)fkRr#Y1UUj#qI#AkIz0+Vq-Y@oydnhkopnQ!5?=*4a#|+t-@Qz3g7cK_~UMX^&tfR8RJ)YrJ4C=+~l7nx$({k z{9)fP^+E859WkcY3|%+)!;^dnl9{L|Mt z+m_7w5I6aQFVAP-k8g70+8x+(RQ=$O2lq4_YvjLv#V$Qx$$x3`CNEz7J0u9`%cdO~ z{Li1dEO4(vZs`AWr!Fb`7SsHNCVwFm&H%j;rQp*>f2DuUxtzu(pvS_+L1CMc_V(Y5v08@IQ9&tlA^k|2Y5fTt)u3 zZrl~}?oQ#6KWrW5T><#tzIjj7FWp=70{=U=?hDf%PyVzW`{R!E-0+uozUoX!{`e-_ zHXKy@7Wuk#q59}my)NC*EXPo9c#1MKrC(#U_ov@|WtI();t^2a;| zPx1#ZoP)8ynfy1T?$R8{e@1Nf+5-NNotLy$-zAa%o^40_Ue6{{4s~C8uOYmX81DsWBr2d!|eZSS-13_Mg9-u zJw(8J_WdmR z5byNJA8SH)&zeBKzF|T0m!CvP8?yzS!{m>P5xP#H*)abvH6g^6YrwZ$=M9wB>w1coO{gvKP%e_zZH|S z8GqCG*DD?2#sB`jhw{$VTMvJm_~V<RMpsh4q0A%E!lw8N1gvV+=z+89-WJ&8zb<8m=0yIh7H!kMK;(Z?`UjMk{^(A;nU+`nS1;dG>w@rMr~vB-Z+WcJ-H_~YCO-#FSX$$#UIyR|Iq@D20IANB&&MgBM|4fos&o8zaK zVZ#Q0r4L9uGFZAZNuE1zmURkq!+(dolgD_a{pZo6C!%#Nn*;e@m3~>& zQ^^0ZEN9*!fPZGjRc*TiAMl4?Lzf1lbeqWEv}rkUN80oFQw zX}c!*@ALQp5*z$|&m-UuJ9qsu*)}=y5AygfP~Ist`J=z+<4XG{)&<@hfj`;mQbns7|pRVg;S*MWvF^BMf z0+s>)gS$?u^^j@cf9Ci_-A1H?Kg!{MH!KJK_zwha)J%sh$9GJF|AZm7ErsR4-?WD^ z-6H*lRnMvcwz#o3D{C^Q~+snPQiN}we zY2a`AtRNlyJJlPZ`-5ejLh#4Ek@RozDnGE(WH)`ZDHqmolraBOf^^I~-*WJ)ZCWWx z`!P|zS>*vae70pdcV**$N+=6`zlO(OhqC@ce0TWmn0In0D|soKnJHIC+Ge@$Gac(= zS)c!!pe}p^mSI@wL+`s+q)U12+rx60-{yB`y1y{@_s@ks1^)W5F#YWr8`3?f@G0zp IJomu=0B%Px*2uVaiR9HvNmwQ;xcNE9p-*RV3N+~k8mBi5FE;N-&Hmls$B)2R@$O_9U`IXRG zy19guOE;S$MWHn^kx>+ryQRs~{ifS*&pEB%etDMlxSzj1=Q+R6=kxlV+vj|RMQY>3 zc#k4{FaUy#0CB^=&JY9vVsWD`z5Xw~)eaN@yx?z%2UIosp_5!^Z(R$Y*(>;9TA%O_L&3v5_JHA%IYOlScD7lH91-E_hx)q4kdQPB0eOHD zaf$i_1{iKMQP}6`iThV`;C?zBQ62#}wev6o$jZthc&Q5mY&;!~h9v_YXt*(If_Vka-gxCW{zAd-52p8ft=Ws>;}$77ed2 zmf}vvbq1&|t3c#Ne+F9*T)ufI}|>7 z$^aDigrEZqpfaEjyuY+%fUW62a~P>I$X1y5RFW^P3W2l-7~qpBu->+g0pfi3A}u%Oe97n;2C&yOL|5hR4B&6=gloy?+18;;XY91y@ERVF0-$)F2|dC9m8Ioy z&^CsZw-a>dPKWzcYm^t2Fu>8RyK&qvlmYsU7!21b^WHoFow!2FsA1*?I~f89bM{8o z+20s|`a-bHcMLGr!U%Q)^(B??zg6(E5J-FAMF6Unq&-m#@bvy;te-HO0SIn`o~5J` z-WovC?ns>49l-!p+V+|TnB(ri093wBmGDjg$UCcsnZnF@DFej%?2%N$djUucK8P4m zQ0qG3tLA`o2sl3m8)2r49eZHkY7x#yop^TuB?X0W96nu=gd)?$xOX+@?E*x51R(v` zX->i$DHq}U^=bx~Fvk?jH4Qp;^E0VD*5xxkmu5F5J2ivdKb5rVyjJ{sy9*p_=KQ&It~$^oiZI+*5x z#Nb1S5e4!*Ag9<3+h(ocfyDFI2smh)psK8#FQE)iJs8UX%XWDRl=V&1QtH7t+`F3H zvIiO(8epTY%M+2d-WXx+7z=b!=*j>ajTYeH^*Jit0U4U0#Lq!eZ7{HToj&*1bl6 zo=W1XvI?QPys_!1^jAmzoyIeoMB;(=ojanYvRd*v)~;g*)KpdT@7i`~kJ^8}d`NH8 zt}W_nYS|{+MwZ>(Br-BGpgbaxk(EV5y_D?*oqqG-e4IA(%R5y52JL>5Ju_A%Q2+n{ M07*qoM6N<$g3?w6$N&HU literal 0 HcmV?d00001 diff --git a/docs/.vuepress/public/fonts/Solina-Bold.woff b/docs/.vuepress/public/fonts/Solina-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..83746eecdd1638509bba87e165949f071e740be2 GIT binary patch literal 17888 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 literal 0 HcmV?d00001 diff --git a/docs/.vuepress/public/fonts/Solina-Bold.woff2 b/docs/.vuepress/public/fonts/Solina-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..813770ef1b25aacbadc5215c9fe8d322a146ddea GIT binary patch literal 16364 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 J00961005SkQ4RnA literal 0 HcmV?d00001 diff --git a/docs/.vuepress/public/fonts/Solina-Light.woff2 b/docs/.vuepress/public/fonts/Solina-Light.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3566766cf5d83e3d93f31c4c49eb406a6bcfa235 GIT binary patch literal 16344 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 aMlDuRruCI-0rUb}wj_)C#52~H(+F1;1H&l* literal 0 HcmV?d00001 diff --git a/docs/.vuepress/public/fonts/Solina-Medium.woff b/docs/.vuepress/public/fonts/Solina-Medium.woff new file mode 100644 index 0000000000000000000000000000000000000000..e9d8fc9b33efdbd1dea7f9b8362414273dec39eb GIT binary patch literal 17684 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